Skip to main content
2 of 2
added 662 characters in body; edited title
Der Kommissar
  • 20.3k
  • 4
  • 70
  • 158

Responding to API requests with much complexity

So having used the SE API multiple times, I like how all responses are in a Wrapper object, and for my own API design I figured it was a good method to follow.

Of course, I also wanted to abstract everything out so that common actions could be handled in a simple manner, so that I wouldn't need to write the same boilerplate code multiple times. (Wrap the entire response generation in a try/catch block then serialize exceptions to JSON, etc.)

Of course, since the inside of the try/catch block differs for each request, I needed a way to deal with that.

I got the idea to use a Request class which had a ProcessRequest() method that would call a DoWork() method which was implemented differently for each request.

Let's start with Request<T>:

public abstract class Request<T>
    where T : IBaseModel
{
    private ResponseType _responseType;

    public Request(HttpContext context)
    {
        var responseTypeString = context.Request.QueryString["FileType"] ?? string.Empty;
        _responseType = Utilities.Extensions.ResponseTypeExtensions.FromString(responseTypeString);
    }

    public string ProcessRequest()
    {
        try
        {
            var response = DoWork();
            var responseWrapped = BuildWrapper(response);
            var responseString = "";

            switch (_responseType)
            {
                // For the Delimited Serializer types, serialize ONLY the Items. The Delimited Serializer doesn't support serializing graph objects like JSON and XML do.
                case ResponseType.Csv:
                    responseString = DelimitedSerializer.CsvSerializer.Serialize(responseWrapped.Items);
                    break;
                case ResponseType.Psv:
                    responseString = DelimitedSerializer.PsvSerializer.Serialize(responseWrapped.Items);
                    break;
                case ResponseType.Tsv:
                    responseString = DelimitedSerializer.TsvSerializer.Serialize(responseWrapped.Items);
                    break;
                // For the JSON and XML types, serailize the entire response.
                case ResponseType.Json:
                    JsonSerialization.Serialize(responseWrapped, ref responseString);
                    break;
                case ResponseType.Xml:
                    XmlSerialization.Serialize(responseWrapped, ref responseString);
                    break;
            }

            return responseString;
        }
        catch (Exception e)
        {
            return Exception(e);
        }
    }

    private static string Exception(Exception exception)
    {
        if (exception is ArgumentException)
        {
            return ArgumentException((ArgumentException)exception);
        }

        return SerializeWrapper(BuildErrorWrapper(new ExceptionResponse(exception)));
    }

    private static string SerializeWrapper<TItem>(ApiResponseWrapper<TItem> wrapper)
        where TItem : IBaseModel
    {
        string response = "";
        JsonSerialization.Serialize(wrapper, ref response);
        return response;
    }

    private static ApiResponseWrapper<TItem> BuildErrorWrapper<TItem>(TItem item)
        where TItem : IBaseModel
    {
        var wrapper = new ApiResponseWrapper<TItem>();
        wrapper.Items.Add(item);
        wrapper.IsError = true;

        AddRateLimits(wrapper);

        return wrapper;
    }

    private static ApiResponseWrapper<TItem> BuildWrapper<TItem>(IEnumerable<TItem> items)
        where TItem : IBaseModel
    {
        var wrapper = new ApiResponseWrapper<TItem>();
        wrapper.Items.AddRange(items);
        wrapper.IsError = false;

        AddRateLimits(wrapper);

        return wrapper;
    }

    private static void AddRateLimits<TItem>(ApiResponseWrapper<TItem> wrapper)
        where TItem : IBaseModel
    {
        wrapper.QuotaMax = int.MaxValue;
        wrapper.QuotaRemaining = int.MaxValue;
    }

    private static string ArgumentException(ArgumentException exception) => SerializeWrapper(BuildErrorWrapper(new ArgumentExceptionResponse(exception)));

    protected abstract IEnumerable<T> DoWork();
}

The idea is to allow a derived class to implement it's own DoWork() method which will be run by the Request<T>.ProcessRequest() method.

To use it, an IHttpHandler has to simply do the following:

public class SiteHistory : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        var request = new SiteHistoryRequest(context);
        var response = request.ProcessRequest();

        context.Response.ContentType = "text/plain";
        context.Response.Write(response);
    }

    public bool IsReusable { get { return false; } }
}

Where SiteHistoryRequest would do all the major work and such.

This also means each type of request can be reused for multiple endpoints, with different parameters specified. (The base Request<T> cares not about any of this, it only cares about the core processing.)

Finally, here's what a sample implementation might look like (this endpoint isn't fully implemented, don't review it yet):

public class SiteHistoryRequest : Request<SiteHistoryResponseItem>
{
    private const string _defaultFields = "Date,TotalQuestions,TotalAnswers,QuestionAnswerRate,AnswerAcceptRate,AnswersPerQuestion";
    private string _fields;
    private string _site;
    public SiteHistoryRequest(HttpContext context)
        : base(context)
    {
        _fields = context.Request.QueryString["Fields"] ?? _defaultFields;
        _site = context.Request.QueryString["Site"];
    }
    protected override ApiResponseWrapper<SiteHistoryResponseItem> DoWork()
    {
        if (string.IsNullOrWhiteSpace(_site))
        {
            throw new ArgumentException("The 'Site' parameter is required and cannot be empty.", "Site");
        }
        return new ApiResponseWrapper<SiteHistoryResponseItem>();
    }
}

This satisfies two of my requirements:

  1. All endpoints can use the same code and general implementation, and satisfy the file-type serialization requirements.
  2. All errors will be gracefully handled and returned to the user as a JSON object.

Any comments welcome, though I would greatly appreciate any comments/advice on the design pattern itself.

Der Kommissar
  • 20.3k
  • 4
  • 70
  • 158