WebAPI
See Asp/Net.
It's sort of like MVC without the V (automatic json/xml content negotiation). Within an MVC website, where your pages are probably consuming just json, normal MVC controller returning JsonResults are easier to manage.
REST
Generally POST is create, PUT is update, PATCH is partial update, but all 3 can create.
HTTP Verb | Response Code | Notes |
---|---|---|
GET | 200 | Commonly GET resource/ returns a list (collection resource) GET resource/1 returns a single by id (item resource) With If-ModifiedSince may return 304 NotModified |
HEAD | 200 | Headers only |
POST | 200 OK 201 Created 204 No Content |
Create new (or general update) The Location: header may contain the URI of the new resource. |
PUT | 200 OK 201 Created 204 No Content |
Replace resource (but can create) |
PATCH | 200 OK 201 Created 204 No Content |
Partial update (not widely used). |
DELETE | 200 OK 202 Accepted (pending) 204 No Content |
Delete resource |
Only POST is not idempotent (PUT and DELETE can be replayed).
Routes
Very much like MVC, but from a HttpConfiguration rather than RouteTable
WebApiConfig.Register(GlobalConfiguration.Configuration);
RouteConfig.RegisterRoutes(RouteTable.Routes);
Then you MapHttpRoute (not MapRoute).
- The default path is "api".
- There's no "{action}" - it's the http method (get/post/put/delete).
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Routing rules:
- Controllers: derive from ApiController
- Public methods except for [NonAction] and derived methods.
- Method name starts with http method (eg HttpGet is Get or GetProduct) or you can use [HttpGet] etc
- Arguments: by default, simple types (int, string) come from the route, complex types from the request body. Add [FromBody] to change this.
API Controllers
- Derived from [System.Web.Http.]ApiController (not System.Web.Mvc.Controller).
- Don't return ActionResult, return void, normal type or an HttpResponseMessage for customization
Simple action:
// GET api/Product/5
public Product Get(int id)
{
var product = _repository.Get(id);
if (product == null)
{
//return 404 (not trapped by [ExceptionFilter])
throw new HttpResponseException(HttpStatusCode.NotFound);
}
return product;
}
JSON.net
The JSON serializer is JSON.net. It may choke on self-referencing entities (Self referencing loop detected for property ...
). You can ignore the property ([JsonIgnore]) or in App_Start/WebApiConfig.cs ignore it globally:
config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling
= Newtonsoft.Json.ReferenceLoopHandling.Ignore;
Sometimes you want enums serialized as strings (not ints).
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public DaysOfTheWeek WeekDay { get; set; }
Set it globally:
var config = GlobalConfiguration.Configuration; //HttpConfiguration
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(
new Newtonsoft.Json.Converters.StringEnumConverter());
Responses - WebApi v1
public HttpResponseMessage Post(Product product)
{
if (!ModelState.IsValid)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); //400
//or ... return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
if (!User.IsInRole("Admin"))
return Request.CreateErrorResponse(HttpStatusCode.Unauthorized, "Return 401"); //401
if (product.Id == null)
throw new HttpResponseException(HttpStatusCode.NotFound); //404
if (product.IsDeleted) //redirect (not a good example in a POST)
{
var link = Url.Link("DefaultApi", new { controller = "DeletedProduct" });
var response = Request.CreateResponse(HttpStatusCode.Redirect); //302
response.Headers.Location = new Uri(link);
return response;
}
if (_productService.Save(product))
{
//for POST return a 201, instead of 200
return Request.CreateResponse(HttpStatusCode.Created, product);
}
//normal 200
return Request.CreateResponse(HttpStatusCode.OK, product);
}
Testing a v1 action using controller.Url means you have to configure routes and the request (ugh):
controller.Configuration = new HttpConfiguration();
controller.Configuration.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional });
Responses - WebApi v2
WebApi v2 has IHttpActionResult and a pretty full set of methods for each HTTP response.
public IHttpActionResult Post(Product product)
{
if (!ModelState.IsValid)
return BadRequest(ModelState); //400
if (!User.IsInRole("Admin"))
return Unauthorized(); //401
if (product.Id == 0)
return NotFound(); //404
if (product.IsDeleted) //redirect (not a good example in a POST)
{
var link = Url.Link("DefaultApi", new { controller = "DeletedProduct" });
return Redirect(link); //302
}
if (_productService.Save(product))
{
//for POST return a 201, instead of 200
return CreatedAtRoute("DefaultApi", new { controller = "Product", id = product.Id }, product);
//Created(location, product) requires a Url.Link
}
//normal 200
return Ok(product); //return a 200 with payload
}
Testing (async version, async Task<IHttpActionResult>:
//act
var result = (await controller.Post(product)) as CreatedAtRouteNegotiatedContentResult<Product>;
//assert
Assert.IsNotNull(result);
Assert.AreNotEqual(0, result.Content.Id);
Cookies
public HttpResponseMessage GetTranslation(string text)
{
const string cookieName = "language";
//read a request cookie
var cookie =
Request.Headers.GetCookies(cookieName).FirstOrDefault();
var language = (cookie != null) ? cookie[cookieName].Value : null;
var translated = Translate(language, text);
var response = Request.CreateResponse(HttpStatusCode.OK, translated);
//response cookie
var responseCookie = new CookieHeaderValue("lastTranslation", text)
{
Expires = DateTimeOffset.Now.AddDays(1),
Domain = Request.RequestUri.Host,
Path = "/"
};
response.Headers.AddCookies(new[] { responseCookie });
return response;
}
Exceptions
- See also MVC/WebApi exception handling
- MVC's [HandleError] does NOT work!
- HttpResponseException: throw new HttpResponseException(HttpStatusCode.NotFound);
- Derive from (abstract) System.Web.Http.Filters.ExceptionFilterAttribute for [HandleError] type functionality. See example
- To add globally, use GlobalConfiguration.Configuration.Filters
- You can also return an HttpError (a dictionary) in the HttpResponseMessage, or throw passing in the ResponseMessage
// GET api/Product/5
public HttpResponseMessage Get(int id)
{
var item = _repository.Get(id);
if (item == null)
{
var err = new HttpError(id + " not found");
return Request.CreateResponse(HttpStatusCode.NotFound, err);
//or...
return Request.CreateErrorResponse(HttpStatusCode.NotFound, id + " not found");
//or...
throw new HttpResponseException(
Request.CreateErrorResponse(HttpStatusCode.NotFound, id + " not found"));
}
return Request.CreateResponse(HttpStatusCode.OK, item);
}
You can easily return the ModelState for validations
public HttpResponseMessage PostProduct(Product item)
{
if (!ModelState.IsValid)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
}
Tracing
Config
public static void Register(HttpConfiguration config)
{
config.Services.Replace(typeof(System.Web.Http.Tracing.ITraceWriter), myTraceWriter);
Write
Configuration.Services.GetTraceWriter().Info(Request, "Category", "message or exception");