static void

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).

config.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

Routing rules:

API Controllers

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

// 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");