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:
- All endpoints can use the same code and general implementation, and satisfy the file-type serialization requirements.
- 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.