ASP.Net WebApi change this default request method mapping with action name - c#

I am Creating a new Web API Controller named Customer.
and this Controller has one Action named "Create"
I couldn't make this Action able to be requested via "GET" HTTP Request
in this form
http://ip:port/api/Customer/Create?userId=x&password=y
except in this method :
public class CustomerController : ApiController
{
[System.Web.Mvc.AcceptVerbs("GET", "POST")]
[System.Web.Mvc.HttpGet]
[ActionName("Create")]
public MISApiResult<List<Branch>> GetCreate(string userID, string password)
{
return new MISApiResult<List<Branch>>() { Result = new List<Branch>() { new Branch() { ID = Guid.NewGuid(), Name = "Branch1" } }, ResultCode = 1, ResultMessage = "Sucess" };
}
}
Is there any other solution to preserve the action name as "Create" as next.
public class CustomerController : ApiController
{
[System.Web.Mvc.AcceptVerbs("GET", "POST")]
[System.Web.Mvc.HttpGet]
public MISApiResult<List<Branch>> Create(string userID, string password)
{
return new MISApiResult<List<Branch>>() { Result = new List<Branch>() { new Branch() { ID = Guid.NewGuid(), Name = "Branch1" } }, ResultCode = 1, ResultMessage = "Sucess" };
}
}
Thanks.
Edit:
Sorry for not being clear at the first time.
According to this answer:
How does a method in MVC WebApi map to an http verb?
There is a default http method according to the action names, if it starts with Get it'll bemapped to GET HTTP Method be default otherwise it will be mapped to POST.
Is there a way to change this default mapping with a custom one so I could map an action named "Create" with "GET" Http Method for testing purpose since this way is faster for development
I tried to put HttpGet Attribute and AcceptVerbs("GET") and it still map the action with POST Http method.
I found a way like I said and it's to change the action method name into GetCreate and then put ActionName attribute with "Create" value.
but is there a way to change the default mapping?
Thanks again.

You can use custom route fore this action:
[HttpGet]
[Route("customer/create")]
public MISApiResult<List<Branch>> Create(string userID, string password)
Don't forget to enable attribute routing during application configuration (this should be added before default route definition):
config.MapHttpAttributeRoutes();
Though I would recommend to follow the conventions and use appropriate HTTP verbs - if you are creating a customer, then by convention you should use POST request to endpoint api/customers. Otherwise your API can be confusing for other people.
Also I would recommend to use IHttpActionResult as the return type of your method:
public IHttpActionResult Post(string userID, string password)
{
if (_userRepository.Exists(userID))
return BadRequest($"User with ID {userID} already exists");
_userRepository.Create(userID, password);
return StatusCode(HttpStatusCode.Created) // or CreatedAtRoute
}
Further reading: Attribute Routing in ASP.NET Web API 2

why dont you specify route. You actual issue is using System.Web.Mvc
use System.Web.Http instead
using System.Web.Http;
[RoutePrefix("api/Customer")]
public class CustomerController : ApiController
{
[HttpGet]
[Route("Create")]
public MISApiResult<List<Branch>> Create(string userID, string password)
{
return new MISApiResult<List<Branch>>() { Result = new List<Branch>() { new Branch() { ID = Guid.NewGuid(), Name = "Branch1" } }, ResultCode = 1, ResultMessage = "Sucess" };
}
}

Related

Can an ASP.NET Core Web API Controller have more than one ControllerRoute?

I have a controller with the following setup for API requests:
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" }
);
This works great for GET requests, but for POST requests my parameters do not seem to have any values. On the frontend, in JavaScript, I can see my parameters in the payload so I know they are there. However, my controller must not be set up correctly to take the POST requests.
Here is my GET request which works, and my POST which doesn't. I believe my issue is that I set the controller route to require the ? and take parameters. However, I still need to do post requests!
GET request:
public ActionResult Contacts([FromQuery] String DD_INPUT)
{
//We have parameters here just in case we want to use them
IEnumerable queryResult;
String query = "Exec dbo.storedprocedure2 #DD_INPUT";
using (var connection = new SqlConnection(connectionString))
{
queryResult = connection.Query(query, new { DD_INPUT = DD_INPUT });
}
return Ok(queryResult);
}
POST request:
[HttpPost]
public ActionResult AddFirm([FromBody] String FIRM_NAME)
{
String query = "exec dbo.storeprocedername #FIRM_NAME";
System.Diagnostics.Debug.WriteLine("value:" + FIRM_NAME);
using (var connection = new SqlConnection(connectionString))
{
var json = connection.QuerySingle<string>(query, new { FIRM_NAME = FIRM_NAME});
return Content(json, "application/json");
}
}
POST Request JavaScript
axios.post(window.location.origin + '/API/AddFirm', {
FIRM_NAME: this.FirmName
}).then(response => {
this.FirmsArray = response.data;
}).catch(error => {
console.log(error.response.data.error);
});
if you want to use [frombody] in your action, you will have to stringify your data
const json = JSON.stringify(this.FirmName);
await axios.post(window.location.origin + '/API/AddFirm', json, {
headers: {
'Content-Type': 'application/json'
}
}).then(response => {
this.FirmsArray = response.data;
}).catch(error => {
console.log(error.response.data.error);
});
I am only not sure about url that you are offering. Usually Api has a different url than application it is called from. And it usually looks like "/MyController/MyAction". Unfortunately you didn't post your controller header.
UPDATE
if you need post several properties using [frombody] action you need to change your action too
Create view model
public class ViewModel
{
public string FirmName {get; set;}
public string Email {get; set;}
}
action
[HttpPost]
public ActionResult AddFirm([FromBody] ViewModel viewModel)
ajax
data: JSON.stringify({FirmName: this.FirmName, Email: this.CurrentEmail}),
Your configuration up top is your service configuration where you configure that all controllers and all endpoints in your controllers have the format of "{controller}/{action}/{id?}".
You can configure how your route is built not only on API level like you did in your example, but also on Controller and Endpoint level:
i.e.
[ApiController]
[Route("{controller}s"}
public class FirmController : ControllerBase
{
[HttpGet]
[Route("/{firmId}/contacts/{contactId}"]
public ActionResult GetContacts([FromRoute] int firmId, [FromRoute] int contactId)
{
...
}
[HttpPost]
public ActionResult AddFirm([FromBody] string firmName)
{
...
}
}
or even better, add a FirmModel for adding a new firm.
Also give https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design a read for properly designing an API.
So, the optimal solution would be this (Changes are explained in code comments):
[HttpPost]
public ActionResult AddFirm(string FIRM_NAME) // Can accept value from body AND query string. If you have more firm names then simply use List<string>/IEnumable<string> to represent it. If you have more parameters you want to pass, then simply write them like this: string FIRM_NAME, int NUM_OF_EMPLYEES and so on.
{
// Why are you using class String, when you can use the string keyword that does exactlly the same thing?
String query = "exec dbo.storeprocedername #FIRM_NAME";
System.Diagnostics.Debug.WriteLine("value:" + FIRM_NAME);
using (var connection = new SqlConnection(connectionString))
{
var json = connection.QuerySingle<string>(query, new { FIRM_NAME = FIRM_NAME});
return Content(json, "application/json"); // When sending JSON answear, be aware that every parameter from object will start with lowercased letter.
}
}
If you incist on using the [FromBody] tag, then you sadlly have to use models.

CreatedAtAction not respecting route template set in controller in .Net Core 3.0

I'm working on building a restful api that returns 201 Created when the appropriate endpoint is hit. In order to do this, I've attempted to use both the CreatedAtAction and the CreatedAtRoute methods, but they both fail to respect the route defined at the top of the controller (or at the top of the action), and instead they use the default route present in the startup.cs. If I name the route at the top of the controller, and use CreatedAtRoute, it will partially respect the route, but it passes the id field as a query string parameter instead of using it as a portion of the route (like the action specifies).
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]", Name = "Organizations")]
public class OrganizationsController : BaseController
{
[HttpGet("{id}", Name = "GetOrganization")]
public async Task<IActionResult> GetOrganization(string id)
{
var command = new GetOrganizationQuery(id);
return Ok(await Mediator.Send(command));
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> CreateOrganization(CreateOrganizationCommand command)
{
var organization = await Mediator.Send(command);
return CreatedAtAction("GetOrganization", "Organizations",
new {id = organization.Id}, organization);
}
}
If I use:
return CreatedAtAction("GetOrganization", "Organizations", new {id = organization.Id}, organization);
// or
return CreatedAtAction("GetOrganization", new {id = organization.Id}, organization);
The location header is location: https://localhost:5001/Organizations/GetOrganization/08d76ebb-95a9-ec4c-7bea-dbb5ff20cf06.
If I use:
return CreatedAtRoute("GetOrganization",new {id = organization.Id}, organization);
I receive an error, No route matches the supplied values..
If I use:
return CreatedAtRoute("Organizations",new {id = organization.Id}, organization);
I get pretty close, but the ID is added as a query string like so: location: https://localhost:5001/api/v1/Organizations?id=08d76ebc-4b79-d9bf-32e9-3cf4202ebce7.
If I try to make the ID an optional parameter on the controller, it gets appended to all actions within the controller, and that is also not a viable solution.
What is the proper approach to get the location to be set to the correct route?
So it turns out that the root of the error is actually the ApiVersion being present in the route. Even though the application is setup to have AssumeDefaultVersionWhenUnspecified set to true, that value does not appear to affect internal routing. In order to get the application to pick up the proper route, I have to specify the api version in the CreatedAtAction method like the following:
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class OrganizationsController : BaseController
{
[HttpGet("{id}")]
public async Task<IActionResult> GetOrganization(string id)
{
var command = new GetOrganizationQuery(id);
return Ok(await Mediator.Send(command));
}
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<IActionResult> CreateOrganization(CreateOrganizationCommand command, ApiVersion version)
{
var organization = await Mediator.Send(command);
return CreatedAtAction(nameof(GetOrganization),
new { id = organization.Id, version = version.ToString() },
organization);
}
}
You can see more here.

Cannot get LinkGenerator to create a path to API action

I am trying to create a link to an API endpoint from inside a Service - outside of a Controller.
Here is the Controller and its base class. I am using API versioning and Areas in ASP.NET Core.
[ApiController]
[Area("api")]
[Route("[area]/[controller]")]
public abstract class APIControllerBase : ControllerBase
{
}
[ApiVersion("1.0")]
public class WidgetsController : APIControllerBase
{
[HttpGet("{id}"]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Widget>> Get(Guid id)
{
// Action...
}
}
API Versioning configuration:
services.AddApiVersioning(options =>
{
options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader
{
ParameterNames = { "api-version", "apiVersion" }
},
new HeaderApiVersionReader
{
HeaderNames = { "api-version", "apiVersion" }
});
});
And where I actually try to use LinkGenerator:
_linkGenerator.GetPathByAction(
_accessor.HttpContext,
action: "Get",
controller: "Widgets",
values: new
{
id = widget.Id,
apiVersion = "1.0"
}
)
I've tried all manner of variations for the LinkGenerator. I've used the HttpContext overload, I've used the overload without it, I've included the apiVersion parameter and omitted it, I've removed [ApiVersion] from the Controller entirely. Everything always comes back null. If I route to a normal MVC Controller like GetPathByAction("Index", "Home") I get a URL like I should though, so I'm think it must be related to my API Areas, or versioning setup.
You're not specifying the area:
_linkGenerator.GetPathByAction(
_accessor.HttpContext,
action: "Get",
controller: "Widgets",
values: new
{
area = "api",
id = widget.Id,
apiVersion = "1.0"
}
)
In case it helps someone else, I had an issue very similar to this but the action was named GetAsync and it turns out you cannot reference the action by it's full name "GetAsync", you have to use it without the suffix so just "Get".

Route attribute Name property

I have ProductsController and OwnersController:
public class ProductsController : ApiController
{
//constructor is here
// GET /api/products
public IHttpActionResult GetProducts()
{
return Ok(new ApiResponse());
}
// GET /api/products/{productCode}
[HttpGet, Route("api/products/{productCode}")]
public IHttpActionResult GetProductByCode(string productCode)
{
return Ok(new ApiResponse());
}
// POST /api/products
public IHttpActionResult PostProduct(Product product /*my class*/)
{
return CreatedAtRoute("DefaultApi", new { id = product.Id }, product);
}
}
It works perfectly.
But now I create second controller and do the same things but I get the error when trying POST method. Another methods work well!
Lets take a look at the code first:
public class OwnersController : ApiController
{
// constructor
// GET /api/owners/{label}
// GET /api/owners/{label}?skip=1&take=4
[Route("api/owners/{label}")]
public IHttpActionResult GetOwnersExamples(string label, int skip=0, int take=10)
{
return Ok(new ApiResponse());
}
// POST /api/owners/{productCode}
//[HttpPost]
[Route("api/owners/{productCode}"/*, Name = "CreateOwner"*/)]
public IHttpActionResult PostOwner(string productCode, Owner owner)
{
return CreatedAtRoute("DefaultApi", new { id = Owner.Id }, owner);
}
}
Error message:
UrlHelper.Link must not return null
My RouteConfig:
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
As I understand problem that CreateAtRoute method must get another RouteName. As you see I can solve the problem by adding Route Name parameter (commented now) and replace "DefaultApi" with "CreateOwner" but it looks like a hack. I believe there is another method to do it avoiding Name Property.
P.S. Looks like my Web API can see only first controller (ProductsController) - any other methods doesn't work if I delete explicit Route [Route("...")]...
As I understand problem that CreateAtRoute method must get another
RouteName. As you see I can solve the problem by adding Route Name
parameter (commented now) and replace "DefaultApi" with "CreateOwner"
but it looks like a hack. I believe there is another method to do it
avoiding Name Property.
Your understanding is almost correct. However you should specify a name not for the current route, but for the one that points to created resource. CreatedAtRoute fills response Location header that should contain a GET-able URI for newly created resource.
Here is a working sample:
[HttpGet]
[Route("api/owners/{id}", Name = "GetOwnerById")]
public IHttpActionResult GetOwner(int id)
{
// Obtain the owner by id here
return Ok(new ApiResponse());
}
[HttpPost]
[Route("api/owners/{productCode}"/*, Name = "CreateOwner"*/)]
public IHttpActionResult PostOwner(string productCode, Owner owner)
{
return CreatedAtRoute("GetOwnerById", new { id = owner.Id }, owner);
}
(Note: For get this example working, you should comment GetOwnersExamples action, otherwise multiple action will match your GET request.)
You said that it looks like a hack, but it is not. CreatedAtRoute takes a route name as argument and you should provide it. How otherwise the proper action will be selected and Location header will be built?
I solve the problem using next steps:
Delete all [RoutePrefix] for controller - let them work by default - it works perfectly for simple requests.
IMPORTANT: check all methods for duplicates! Problem was I had 2 methods with routes api/controller/{label} and api/controller/{parameter} - it can't understand which of them to use by default. (Or use explicit uri: api/controller?label=1)
IMPORTANT: avoid to put into api methods a lot of complex types - create Wrapper for them and put only one parameter!
All this actions let me delete excess attributes and make methods more readable.
Here is the result:
public IHttpActionResult PostOwner(OwnerWrapper modelWrapper)
{
string productCode = modelWrapper.Product.Code;
Owner owner = modelWrapper.Owners[0];
return CreatedAtRoute("DefaultApi", new { id = Owner.Id }, owner);
}
It is just test case, so we can see productCode is never used, but my real realization is more difficult.

Get the full route to current action

I have a simple API with basic routing. It was setup using the default Visual Studio 2015 ASP.NET Core API template.
I have this controller and action:
[Route("api/[controller]")]
public class DocumentController : Controller
{
[HttpGet("info/{Id}")]
public async Task<Data> Get(string Id)
{
//Logic
}
}
So to reach this method, I must call GET /api/document/info/some-id-here.
Is it possible with .NET Core, inside that method, to retrieve as a string the complete route?
So I could do for example:
var myRoute = retrieveRoute();
// myRoute = "/api/document/info/some-id-here"
You can get the complete requested url using the Request option (HttpRequest) in .Net Core.
var route = Request.Path.Value;
Your final code.
[Route("api/[controller]")]
public class DocumentController : Controller
{
[HttpGet("info/{Id}")]
public async Task<Data> Get(string Id)
{
var route = Request.Path.Value;
}
}
Result route: "/api/document/info/some-id-here" //for example
You can also ask MVC to create a new route URL based on the current route values:
[Route("api/[controller]")]
public class DocumentController : Controller
{
[HttpGet("info/{Id}")]
public async Task<Data> Get(string Id)
{
//Logic
var myRoute = Url.RouteUrl(RouteData.Values);
}
}
Url.RouteUrl is a helper method that lets you build a route URL given any route values. RouteData.Values gives you the route values for the current request.
If you want the original route template on an API controller that was specified with any HttpMethod attribute then this will do it:
var routeAttribute = Url.ActionContext.ActionDescriptor.EndpointMetadata.First(d => d is HttpMethodAttribute);
var routeTemplate = ((HttpMethodAttribute)routeAttribute).Template;
If the original route attribute was: [HttpGet("Self/{id}")]
The routeTemplate value would be: "Self/{id}"
Url.ActionContext.ActionDescriptor.AttributeRouteInfo.Template
"v{version}/{cardId}/cardsTest" {$1}

Categories

Resources