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
Related
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 am having some ActionFilters in my asp.net core mvc application, which validate some input data. For example, the client sends a userId inside the header, the filter loads that user from a repository, and validates, if the user exists, is active, has a license, and so on.
This filter is attached to a Controller method. The Controller method also needs to collect the same user object. Because of performance, I want to pass that user object, collected inside the filter, to the controller, so the controller does not need to load the same user object again. I know there are ways to do so, like mentioned here.
Because of clean code, I wonder if this would be possible, coding an attribute which defines what to retrieve, like the [FromBody] attribute does, for instance.
I could imagine this attribute named [FromFilter("User")], which takes a parameter to specify the key inside the HttpContext.Items
A basic implementation could be something like this:
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromFilterAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
/// <inheritdoc />
public BindingSource BindingSource => BindingSource.Custom;
/// <inheritdoc />
public string Name { get; set; }
}
Neither do I know if this would a be a good idea, nor how to implement such a feature. Hopefully someone can me point into the right direction
As far as I know, we couldn't directly pass the object from filter to action.
In my opinion, the best solution is creating a custom model binding and then find the user from the repository and pass the user to the action.
Since the model binding is triggered before the filter, you could get the custom model binding result from the ActionExecutingContext.
Order of execution:
UserModelBinder --> OnActionExecuting --> Index action
More details about to do it, you could refer to below codes:
Custom model binding:
public class UserModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var model = new UserModel()
{
id = 1,
name = "test"
};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Controller action and OnActionExecuting method:
OnActionExecuting:
public override void OnActionExecuting(ActionExecutingContext context)
{
//ActionArguments["user"] is the parameter name of the action parameter
var user = context.ActionArguments["user"] as UserModel;
// Do something before the action executes.
base.OnActionExecuting(context);
}
Action method:
public async Task<IActionResult> Index([ModelBinder(BinderType = typeof(UserModelBinder))] UserModel user)
{
int i =0;
return View();
}
Result:
Filter onexecuting:
Action parameter:
You can use HttpContext.Items for this and create HttpContextItemsModelBinder which will bind model from HttpContext.Items
public class HttpContextItemsModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var items = bindingContext.HttpContext.Items;
string name = bindingContext.BinderModelName ?? bindingContext.FieldName;
bindingContext.Result = items.TryGetValue(name, out object item)
? ModelBindingResult.Success(item)
: ModelBindingResult.Failed();
return Task.CompletedTask;
}
}
Create and register model binder provider
public static class CustomBindingSources
{
public static BindingSource HttpContextItems { get; } = new BindingSource("HttpContextItems", "HttpContext Items", true, true);
}
public class HttpContextItemsModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.BindingInfo.BindingSource == CustomBindingSources.HttpContextItems)
{
return new HttpContextItemsModelBinder();
}
return null;
}
}
In Startup.cs
services
.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new HttpContextItemsModelBinderProvider());
//...
})
Create an attribute which will set correct BindingSource to use HttpContextItemsModelBinder
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromHttpContextItemsAttribute : Attribute, IBindingSourceMetadata, IModelNameProvider
{
public string Name { get; set; }
public BindingSource BindingSource => CustomBindingSources.HttpContextItems;
public FromHttpContextItemsAttribute(string name)
{
Name = name;
}
public FromHttpContextItemsAttribute() { }
}
Usage:
//in controller
[HttpGet]
[ValidateUserFilter]
public IActionResult TestHttpContextItems([FromHttpContextItems("UserItem")]UserItemModel model)
{
return Ok(model);
}
//your action filter
public class ValidateUserFilterAttribute : ActionFilterAttribute, IAuthorizationFilter
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//...
}
public void OnAuthorization(AuthorizationFilterContext context)
{
var model = new UserItemModel
{
Id = 45,
Name = "Some user name"
};
context.HttpContext.Items["UserItem"] = model;
}
}
Important note
Pay attention that I save user model to HttpContext.Items during OnAuthorization and not OnActionExecuting because model binding happens before any action filters run, so HttpContext.Items won't contain user and model binding will fail. You might need to adjust filter code to your needs and to make the solution work as expected.
Usage without specifying item name. Parameter name in action method should match key ("userModel") used to store value in HttpContext.Items:
//in controller
[HttpGet]
[ValidateUserFilter]
public IActionResult TestHttpContextItems([FromHttpContextItems]UserItemModel userModel)
{
return Ok(userModel);
}
//action filter
public class ValidateUserFilterAttribute : ActionFilterAttribute, IAuthorizationFilter
{
public override void OnActionExecuting(ActionExecutingContext context)
{
//...
}
public void OnAuthorization(AuthorizationFilterContext context)
{
//...
context.HttpContext.Items["userModel"] = model;
}
}
I'm writing an API for my game and I'm starting to realize that the amount of GET, POST, and PUT API methods can really add up.
So right now, I'm trying to make it more generic so that I don't have to write a separate method like GetMonsterList, GetTreasureList, GetPlayerInfo, etc.
But I'm not quite sure how to go about doing that.
Here is a non-generic PUT method that I currently have.
// PUT: api/MonsterLists/5
[HttpPut("{id}")]
public async Task<IActionResult> PutMonsterList(string id, MonsterList monsterList)
{
if (id != monsterList.MonsterId)
{
return BadRequest();
}
_context.Entry(monsterList).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!MonsterListExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return NoContent();
}
And here is my attempt at outlining a generic method:
// PUT: api/AnyLists/5
[HttpPut("{id}")]
public async Task<IActionResult> PutAnyList(string id, AnyList anyList)
{
if (id != anyList.AnyId)
{
return BadRequest();
}
_context.Entry(anyList).State = EntityState.Modified;
return NoContent();
}
My problem that I don't understand is, how do I pass in a model to a generic control like this? Like if I have a model for MonsterList, TreasureList, PlayerInfo, WeaponList, etc.
How could I use one generic method for all of them?
I did find one similiar question here, Generic Web Api controller to support any model , but the answer seemed to imply that this isn't a good idea.
Is that possible?
Thanks!
Before we create the generic controller, it is worth to mention that the structure model of your entities is so important to easily or hardly build the generic controller.
For example you could have some models with int id and others with string id, so we need to have a common base for both types.
Start by creating the common interface for Id property to handle int or string Ids in the generic interface:
public interface IHasId<TKey>
where TKey : IEquatable<TKey>
{
TKey Id { get; set; }
}
Another thing to consider is ordering the entities, when querying for a list of entities we need to sort them to get the right paged entities. So, we can create another interface to specify the sorting property e.g. Name.
public interface IOrdered
{
string Name { get; set; }
}
Our objects must implement the common interfaces like below:
public class Player : IHasId<string>, IOrdered
{
public string Id { get; set; }
public string Name { get; set; }
...
}
public class Treasure : IHasId<int>, IOrdered
{
public int Id { get; set; }
public string Name { get; set; }
...
}
Now create a generic base api controller, make sure to mark the methods as virtual so we can override them in the inherited api controllers if necessary.
[Route("api/[controller]")]
[ApiController]
public class GenericBaseController<T, TKey> : ControllerBase
where T : class, IHasId<TKey>, IOrdered
where TKey : IEquatable<TKey>
{
private readonly ApplicationDbContext _context;
public GenericBaseController(ApplicationDbContext context)
{
_context = context;
}
// make methods as virtual,
// so they can be overridden in inherited api controllers
[HttpGet("{id}")]
public virtual T Get(TKey id)
{
return _context.Set<T>().Find(id);
}
[HttpPost]
public virtual bool Post([FromBody] T value)
{
_context.Set<T>().Add(value);
return _context.SaveChanges() > 0;
}
[HttpPut("{id}")]
public virtual bool Put(TKey id)
{
var entity = _context.Set<T>().AsNoTracking().SingleOrDefault(x => x.Id.Equals(id));
if (entity != null)
{
_context.Entry<T>(value).State = EntityState.Modified;
return _context.SaveChanges() > 0;
}
return false;
}
[HttpDelete("{id}")]
public virtual bool Delete(TKey id)
{
var entity = _context.Set<T>().Find(id);
if (entity != null)
{
_context.Entry<T>(entity).State = EntityState.Deleted;
return _context.SaveChanges() > 0;
}
return false;
}
[HttpGet("list/{pageNo}-{pageSize}")]
public virtual (IEnumerable<T>, int) Get(int pageNo, int pageSize)
{
var query = _context.Set<T>();
var totalRecords = query.Count();
var items = query.OrderBy(x => x.Name)
.Skip((pageNo - 1) * pageSize)
.Take(pageSize)
.AsEnumerable();
return (items, totalRecords);
}
}
The rest is easy, just create api controllers that inherits from the base generic controller:
PlayersController :
[Route("api/[controller]")]
[ApiController]
public class PlayersController : GenericBaseController<Player, string>
{
public PlayersController(ApplicationDbContext context) : base(context)
{
}
}
TreasuresController :
[Route("api/[controller]")]
[ApiController]
public class TreasuresController : GenericBaseController<Treasure, int>
{
public TreasuresController(ApplicationDbContext context) : base(context)
{
}
}
you don't have to create any methods, but you are still able to override the base methods since we marked them as virtual e.g.:
[Route("api/[controller]")]
[ApiController]
public class TreasuresController : GenericBaseController<Treasure, int>
{
public TreasuresController(ApplicationDbContext context) : base(context)
{
public ovedrride Treasure Get(int id)
{
// custom logic ….
return base.Get(id);
}
}
}
You can download a sample project from GitHub: https://github.com/LazZiya/GenericApiSample
I guess you can pass over the name of the type of the parameter and do something like this (not tested):
// PUT: api/AnyLists/5
[HttpPut("{id}")]
public async Task<IActionResult> PutAnyList(string id, object anyList, string anyListType)
{
var anyListObject = Convert.ChangeType(anyList, Type.GetType(anyListType)));
if (id != anyListObject.AnyId)
{
return BadRequest();
}
_context.Entry(anyListObject).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
// Whatever error handling you need
}
return NoContent();
}
However, I wouldn't recommend to use this in production code. What will likely happen is that you will need to create quite a lot of exceptions for different types in the end - and you'll end up with the code that is much more convoluted and hard to support than if you just had separate methods per type.
Also, I'm not sure it will be easy to test this.
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}