I am trying to do something similar to what is suggested on this site
https://mathieu.fenniak.net/stop-designing-fragile-web-apis/
It suggested that this is a better url
http://api.fbi.gov/wanted/most
My question is how do I do something like that in ASP.NET WEBAPI. For example, if I want to return a specific query of joining some data with another table, instead of passing in a parameter I just want a method url call that just does the one query I want. What is the easiest way to accomplish this task?
example url call:
api/controller/joinresultwithtable2
It's not exactly pretty but you can setup routing so a specific path maps to a specific controller and action.
config.Routes.MapHttpRoute("query1", "query/query1", new {controller="StockQueries", action="query1"});
config.Routes.MapHttpRoute("query2", "query/query2", new { controller = "StockQueries", action = "query2" });
config.Routes.MapHttpRoute("query3", "query/query3", new { controller = "StockQueries", action = "query3" });
And then have a controller that looks like this,
public class StockQueriesController : ApiController
{
[ActionName("query1")]
public HttpResponseMessage GetQuery1()
{
return new HttpResponseMessage() {Content = new StringContent("Query1")};
}
[ActionName("query2")]
public HttpResponseMessage GetQuery2()
{
return new HttpResponseMessage() { Content = new StringContent("Query1") };
}
[ActionName("query3")]
public HttpResponseMessage GetQuery3()
{
return new HttpResponseMessage() { Content = new StringContent("Query1") };
}
[ActionName("query4")]
public HttpResponseMessage GetQuery4()
{
return new HttpResponseMessage() { Content = new StringContent("Query1") };
}
}
The easiest way to go is probably attribute routing.
You can find more information here: http://attributerouting.net/
It allows you to declare any route right (with parameters) directly on an action method. That way you can control quite easily how you make your resources available.
In case you want to version your API, it's also quite easy, because you can just include the version in your attribute
If you are using ASP.NET Web API 2, you can do the following:
[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
Source: http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
You can also use following method:
[ActionName("DefaultApi")]
[Route("Api/UserLogin/DefaultApi/UserDetails")]
public IHttpActionResult UserDetails(){
return Ok(db.UserLogins.ToList());
}
Related
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.
How to have a Route which points to two different controller end points which accepts different arguments in WEB Api 2
I have two different end points declared in controller and as for the REST perspective I have to use the alpha/{aplhaid}/beta format for both the end points ,
[Authorize]
[HttpPost]
[Route("alpha/{aplhaid}/beta")]
public async Task<HttpResponseMessage> CreateAlpha(Beta beta, string projectId, [FromHeader] RevisionHeaderModel revision)
[Authorize]
[HttpPost]
[Route("alpha/{aplhaid}/beta")]
public async Task<HttpResponseMessage> CreateAlpha(List<Beta> betas, string projectId, [FromHeader] RevisionHeaderModel revision)
Is it possible to use the same router with different parameters which points to 2 different end points in Web API 2?
If you really need to have the same route and the same ActionName, you could do it with an IHttpActionSelector.
public class CustomActionSelector : ApiControllerActionSelector, IHttpActionSelector
{
public new HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
var context = HttpContext.Current;
// Read the content. Probably a better way of doing it?
var stream = new StreamReader(context.Request.InputStream);
var input = stream.ReadToEnd();
var array = new JavaScriptSerializer().Deserialize<List<string>>(input);
if (array != null)
{
// It's an array
//TODO: Choose action.
}
else
{
// It's not an array
//TODO: Choose action.
}
// Default.
var action = base.SelectAction(controllerContext);
return action;
}
public override ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
{
var lookup = base.GetActionMapping(controllerDescriptor);
return lookup;
}
}
In your WebApiConfig:
config.Services.Replace(
typeof(IHttpActionSelector),
new CustomActionSelector());
Example for your an Controller:
public class FooController: ApiController
{
[HttpPost]
public string Post(string id)
{
return "String";
}
[HttpPost]
public string Post(List<string> id)
{
return "some list";
}
}
The solution has some big downsides, if you ask me. First of, you should look for a solution for using this CustomActionSelector only when needed. Not for all controllers as it will create an overhead for each request.
I think you should reconsider why you really need two have to identical routes. I think readability will be suffering if the same route accepts different arguments. But that's just my opinion.
I would use different routes instead.
Overload web api action method based on parameter type is not well supported.
But what about attribute based routing ?
You can find out a good example here
Route constraints let you restrict how the parameters in the route template are matched. The general syntax is "{parameter:constraint}". For example:
[Route("users/{id:int}"]
public User GetUserById(int id) { ... }
[Route("users/{name}"]
public User GetUserByName(string name) { ... }
And I think this link must be helpful
Use one route and call the other controller inside from the first controller.
I am new to web api coming from a WCF background and as prep I watched Shawn Wildermuth's Pluralsight course on the subject before diving in. His course material was designed around more traditional routing. One of the subjects the course dives into is HATEOAS and how easy it is to achieve this with a base api controller and model factory.
One of the first things I hit when implementing against attribute routing was the need for the UrlHelper to have a route name as the first argument of the Link() method, something that was inherited in the conventional routing configured in the WebApiConfig.cs.
I worked around this by decorating one of my controllers route attributes with the Name property and it appears that all methods in that controller have access to the name property regardless of which method I put it on (see code below). While I find this a bit odd, it works. Now that I had HATEOAS implemented, I noticed the URL's it was generating were in the query string format and not "url" formatted (I know the term is wrong but bear with me). Instead of .../api/deliverables/1 I am getting .../api/deliverables?id=1.
This is "ok" but not the desired output. While I still have not figured out how to adjust the formatting the of the return value of the URL, I figured I would test the query string against my controller and found that in the query string format my controller does not work but in the "url" format it does.
I then spent an hour trying to figure out why. I have attempted different decorations (i.e. [FromUri] which from my reading should only be necessary for complex objects which default to the message body) to setting default values, constraints and making it optional (i.e. {id?}).
Below is the code in question, both for the controller, the base api controller and the model factory that makes the HATEOAS implementation possible.
The 3 questions I have are:
1) How to make the controller accept the "id" on the querystring AND in the url format (.../deliverables/1 and .../deliverables?id=1.
2) How to make the Link method of the URL helper return the value in the url format (it is currently returning it as a query string.
3) Proper way to name routes in WebAPI 2. What I am doing (assigning a name to a single method and the others appear to inherit it simply smells and I have to believe this would crumble as I actually start to implement more complex code. Is Shawn's implementation flawed in some way? I like not having to hard code a URL for test/development purposes but maybe UrlHelper is not the best way to achieve this. It seems to carry with it a lot of baggage that may not be necessary.
Controller:
[RoutePrefix("api/deliverables")]
public class DeliverablesController : BaseApiController
{
private readonly IDeliverableService _deliverableService;
private readonly IUnitOfWork _unitOfWork;
public DeliverablesController(IDeliverableService deliverableService, IUnitOfWorkAsync unitOfWork)
{
_deliverableService = deliverableService;
_unitOfWork = unitOfWork;
}
[Route("", Name = "Deliverables")]
public IHttpActionResult Get()
{
return Ok(_deliverableService.Get().Select(TheModelFactory.Create));
}
[Route("{id}")]
public IHttpActionResult Get(int id)
{
return Ok(TheModelFactory.Create(_deliverableService.Find(id)));
}
[Route("")]
public IHttpActionResult Post([FromBody]DeliverableModel model)
{
try
{
var entity = TheModelFactory.Parse(model);
if (entity == null)
{
return BadRequest("Could not parse Deliverable entry in body.");
}
_deliverableService.Insert(entity);
_unitOfWork.SaveChanges();
return Created(Request.RequestUri + "/" + entity.Id.ToString(CultureInfo.InvariantCulture),TheModelFactory.Create(entity));
}
catch (Exception exception)
{
return BadRequest(exception.Message);
}
}
}
Base API Controller:
public abstract class BaseApiController : ApiController
{
private ModelFactory _modelFactory;
protected ModelFactory TheModelFactory
{
get
{
return _modelFactory ?? (_modelFactory = new ModelFactory(Request));
}
}
}
Model Factory:
public class ModelFactory
{
private readonly UrlHelper _urlHelper;
public ModelFactory(HttpRequestMessage request)
{
_urlHelper = new UrlHelper(request);
}
public DeliverableModel Create(Deliverable deliverable)
{
return new DeliverableModel
{
Url = _urlHelper.Link("deliverables", new { id = deliverable.Id }),
Description = deliverable.Description,
Name = deliverable.Name,
Id = deliverable.Id
};
}
public Deliverable Parse(DeliverableModel model)
{
try
{
if (string.IsNullOrEmpty(model.Name))
return null;
var entity = new Deliverable
{
Name = model.Name,
Description = !string.IsNullOrEmpty(model.Description)
? model.Description
: string.Empty
};
return entity;
}
catch (Exception)
{
return null;
}
}
}
As a point of clarification, non-attribute (traditional) routing works without an issue for both the URI and query string formats:
config.Routes.MapHttpRoute(
name: "deliverables",
routeTemplate: "api/deliverables/{id}",
defaults: new { controller = "deliverables", id = RouteParameter.Optional }
);
In my opinion, this is one of the problems with Attributed routing. That's why I use it for exceptional cases only. I use route tables for the majority of routing then drop down into attributed routing for exceptional cases.
To solve this your way, have you thought about multiple routes on the Get(id)? (I don't actually think this would work, but its worth a try).
I have an MVC API controller with the following action.
I don't understand how to read the actual data/body of the Message?
[HttpPost]
public void Confirmation(HttpRequestMessage request)
{
var content = request.Content;
}
From this answer:
[HttpPost]
public void Confirmation(HttpRequestMessage request)
{
var content = request.Content;
string jsonContent = content.ReadAsStringAsync().Result;
}
Note: As seen in the comments, this code could cause a deadlock and should not be used. See this blog post for more detail.
using System.IO;
string requestFromPost;
using( StreamReader reader = new StreamReader(HttpContext.Current.Request.InputStream) )
{
reader.BaseStream.Position = 0;
requestFromPost = reader.ReadToEnd();
}
I suggest that you should not do it like this.
Action methods should be designed to be easily unit-tested. In this case, you should not access data directly from the request, because if you do it like this, when you want to unit test this code you have to construct a HttpRequestMessage.
You should do it like this to let MVC do all the model binding for you:
[HttpPost]
public void Confirmation(YOURDTO yourobj)//assume that you define YOURDTO elsewhere
{
//your logic to process input parameters.
}
In case you do want to access the request. You just access the Request property of the controller (not through parameters). Like this:
[HttpPost]
public void Confirmation()
{
var content = Request.Content.ReadAsStringAsync().Result;
}
In MVC, the Request property is actually a wrapper around .NET HttpRequest and inherit from a base class. When you need to unit test, you could also mock this object.
In case you want to cast to a class and not just a string:
YourClass model = await request.Content.ReadAsAsync<YourClass>();
I have a problem with my custom RouteBase implementation and [OutputCache].
We have a CMS in which urls are mapped to certain content pages. Each type of content page is handled by a different controller (and different views). The urls are completely free and we want different controllers, so a "catchall" route is not usable. So we build a custom RouteBase implementation that calls the database to find all the urls. The database knows which Controller and Action to use (based on the content page type).
This works just great.
However, combining this with the [OutputCache] attribute, output caching doesn't work (the page still works). We made sure [OutputCache] works on our "normal" routes.
It is very difficult to debug outputcaching, the attribute is there we us it, it doesn't work... Ideas how to approach this would be very welcome, as would the right answer!
The controller looks like this:
public class TextPageController : BaseController
{
private readonly ITextPageController textPageController;
public TextPageController(ITextPageController textPageController)
{
this.textPageController = textPageController;
}
[OutputCache(Duration = 300)]
public ActionResult TextPage(string pageid)
{
var model = textPageController.GetPage(pageid);
return View(model);
}
}
The custom route looks like this:
public class CmsPageRoute : RouteBase
{
private IRouteService _routeService;
private Dictionary<string, RouteData> _urlsToRouteData;
public CmsPageRoute(IRouteService routeService)
{
this._routeService = routeService;
this.SetCmsRoutes();
}
public void SetCmsRoutes()
{
var urlsToRouteData = new Dictionary<string, RouteData>();
foreach (var route in this._routeService.GetRoutes()) // gets RouteData for CMS pages from database
{
urlsToRouteData.Add(route.Url, PrepareRouteData(route));
}
Interlocked.Exchange(ref _urlsToRouteData, urlsToRouteData);
}
public override RouteData GetRouteData(System.Web.HttpContextBase httpContext)
{
RouteData routeData;
if (_urlsToRouteData.TryGetValue(httpContext.Request.Path, out routeData))
return routeData;
else
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
private RouteData PrepareRouteData(ContentRouteData contentRoute)
{
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values.Add("controller", contentRoute.Controller);
routeData.Values.Add("action", contentRoute.Action);
routeData.Values.Add("area", contentRoute.Area);
routeData.Values.Add("pageid", contentRoute.Constraints["pageid"]); // variable for identifying page id in controller method
routeData.DataTokens.Add("Namespaces", new[] { contentRoute.Namespace });
routeData.DataTokens.Add("area", contentRoute.Area);
return routeData;
}
// routes get periodically updated
public void UpdateRoutes()
{
SetCmsRoutes();
}
}
Thank you for reading until the end!
In the end we tracked it down to a call to
... data-role="#this.FirstVisit" ...
in our _Layout.cshtml
This called a property on our custom view page that in turn called a service which always set a cookie. (Yikes setting cookies in services!, we know!)
Had it not been Friday and at the end of the sprint we might have noticed the cache busting
Cache-Control: no-cache="Set-Cookie": Http Header.
I still don't understand why this only busted the cache for our custom RouteBase implementation and not all pages. All pages use the same _Layout.cshtml.
You can try the following code if it helps
[OutputCache(Duration = 300, VaryByParam="*")]
public ActionResult TextPage(string pageid)
{
var model = textPageController.GetPage(pageid);
return View(model);
}
You might look here to customize caching :
Ignore url values with Outputcache
Output Cache problem on ViewEngine that use 2 separate view for 1 controller
Also, check the cache location.