dcastro / JSendWebApi

MIT License
10 stars 5 forks source link

JSend.WebApi & JSend.Client

JSendApiController extends ASP.NET Web API 2's ApiController and enables easy generation of JSend-formatted responses.

JSendClient complements the controller, by wrapping around HttpClient and providing an easy way to send HTTP requests and parse JSend-formatted responses.

Example

JSend.WebApi

public class ArticlesController : JSendApiController
{
    public IHttpActionResult Get(int id)
    {
        var article = _repo.Get(id);

        if (article != null)
            return JSendOk(article);

        return JSendNotFound();
    }
}

JSend.Client

using (var client = new JSendClient())
{
    var getResponse = await client.GetAsync<Article>("http://localhost/articles/4");
    var existingArticle = getResponse.GetDataOrDefault();

    var postResponse = await client.PostAsync<Article>("http://localhost/articles/", article);
    var newArticle = postResponse.Data;

    var deleteResponse = await client.DeleteAsync("http://localhost/articles/4");
    deleteResponse.EnsureSuccessStatus(); //throws if the response's status is not "success"

    var putResponse = await client.PutAsync("http://localhost/articles/4", existingArticle);
    if (! putResponse.IsSuccess)
        Logger.Log(putResponse.Error);
}

JSend.WebApi

Return types

The return value of a JSendApiController action is converted to a HTTP response as follows:

Void actions

Actions that don't return anything are converted to a 200 response with its status set to success.

public class ArticlesController : JSendApiController
{
    public void Post()
    {
    }
}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
    "status" : "success",
    "data" : null
}

IHttpActionResult

The JSendApiController provides several helper methods to easily build JSend-formatted responses.
Here's a full list of these helpers methods and examples responses.

A simple example:

public class ArticlesController : JSendApiController
{
    private readonly IRepository<Article> _repo = //...;

    public IHttpActionResult Post(Article article)
    {
        if (!ModelState.IsValid)
            return JSendBadRequest(ModelState); // Returns a 400 "fail" response

        _repo.Store(article);
        return JSendCreatedAtRoute("GetArticle", new {id = article.Id}, article); // Returns a 201 "success" response
    }

    [Route("articles/{id:int}", Name = "GetArticle")]
    public IHttpActionResult Get(int id)
    {
        var article = _repo.Get(id);
        return JSendOk(article); // Returns a 200 "success" response
    }
}

The Post action above will return one of the following HTTP responses:

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: http://localhost/articles/5

{
    "status" : "success",
    "data" : {
        "title" : "Ground-breaking study discovers how to exit Vim"
    }
}
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8

{
    "status" : "fail",
    "data" : { 
        "article.Title" : "The Title field is required."
    }
}

Other return types

For all other return types (*), they'll be wrapped in a 200 response with its status set to success ,

public class ArticlesController : JSendApiController
{
    public IEnumerable<Article> Get()
    {
        return GetAllArticlesFromDb();
    }
}
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
    "status": "success",
    "data": [
        {
            "title": "Why Are So Many Of The Framework Classes Sealed?"
        },     
        {
            "title": "C# Performance Benchmark Mistakes, Part One"
        }
    ]
}

(*) Except HttpResponseMessage, which is converted directly to an HTTP response.

Exceptions

Depending on the current IncludeErrorDetailPolicy and on whether the client is local or remote, exceptions thrown by JSendApiController actions will be formatted as either:

HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=utf-8

{                                                                                                                       
  "status": "error",                                                                                                    
  "message": "Operation is not valid due to the current state of the object.",                                          
  "data": "System.InvalidOperationException: Operation is not valid due to the current state of the object.
}

or as:

HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=utf-8

{                                               
  "status": "error",                            
  "message": "An error has occurred."           
}

The default behavior is to show exception details to local clients and hide them from remote clients.

Other stuff

JSend.Client

Querying an API

await client.GetAsync<Article>(uri);

await client.PostAsync<Article>(uri, article);   // If you expect an updated article back
await client.PostAsync(uri, article);            // If you don't expect data back

await client.PutAsync<Article>(uri, article);    // If you expect an updated article back
await client.PutAsync(uri, article);             // If you don't expect data back

await client.DeleteAsync(uri);

Handling the response

If you expect the API to always return a "success response"...

//... and you don't need any data
response.EnsureSuccessStatus();  //(throws if response was not successful)

//... and you expect the response to always contain data
var article = response.Data;     //(throws if response was not successful or did not contain data)

//... and you're not sure whether the response contains data
var article = response.GetDataOrDefault();
var article = response.GetDataOrDefault(new Article());
if (response.HasData) { ... }

If the API might return a "fail/error response" (e.g., because a resource was not found)...

//... and you don't need any data
if (response.IsSuccess) { ... }

//... and you need the data
var article = response.GetDataOrDefault();
var article = response.GetDataOrDefault(new Article());
if (response.HasData) { ... }

//... and you want to handle the error
if (! response.IsSuccess) { Logger.Log(response.Error); }

If you want to know more details about the response, such as its status code, you can use the JSendResponse.HttpResponse property to access the original HTTP response message.

if (response.HttpResponse.StatusCode == HttpStatusCode.NotFound)
{
    ...
}

Configuring the client

If you want the client to perform some additional work (e.g., add a "X-Message-Id" header to all requests, or log all exceptions) you can do so by extending MessageInterceptor:

class MessageIdInterceptor : MessageInterceptor
{
    public override void OnSending(HttpRequestMessage request)
    {
        request.Headers.Add("X-Message-Id", Guid.NewGuid().ToString());
    }
}

class LoggerInterceptor : MessageInterceptor
{
    public override void OnException(ExceptionContext context)
    {
        Logger.Log(context.Exception);
    }
}

You can then configure the client like this:

var interceptor = new CompositeMessageInterceptor(
    new MessageIdInterceptor(),
    new LoggerInterceptor());

var settings = new JSendClientSettings
    {
        MessageInterceptor = interceptor,
        SerializerSettings = new JsonSerializerSettings
            {
                Formatting = Formatting.Indented
            }
    };

var client = new JSendClient(settings);

Download

To install JSend.WebApi, run the following command in the Package Manager Console

PM> Install-Package JSend.WebApi

To install JSend.Client, run the following command in the Package Manager Console

PM> Install-Package JSend.Client

Or download the binaries/source code from here.