Skip to content

Instantly share code, notes, and snippets.

@Boggin
Last active January 19, 2016 15:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Boggin/5f31ae869069cfcc2e59 to your computer and use it in GitHub Desktop.
Save Boggin/5f31ae869069cfcc2e59 to your computer and use it in GitHub Desktop.
A WebAPI Controller for an Azure WebRole that can manage a request that will take a long time to fulfill.
namespace WebRole.Controllers
{
public class MyController : ApiController
{
private readonly ICache cache;
private readonly IMyRequestQueue myRequestQueue;
public MyController(
ICache cache,
IModellerRequestQueue myRequestQueue)
{
this.cache = cache;
this.myRequestQueue = myRequestQueue;
}
public HttpResponseMessage Get([FromUri] MyRequest myRequest)
{
// the client's request is placed on the queue and they receive a
// 202 (Accepted) and an ETag (EntityTag). They poll with the ETag in
// their If-None-Match header. If the resource is not available
// yet they will receive a 304 (Not-Modified) and they must continue
// to poll. When the response is available then it will be written
// into the cache over the value of the original request. Now the ETag
// points to the result. On the next request the client will receive
// a 200 (OK) and their resource.
// try to get the request's ETag.
var clientETag = this.Request.Headers.IfNoneMatch.FirstOrDefault();
// if the If-None-Match header is supplied then
// this is a polling request for the resource.
if (clientETag != null)
{
return this.HandlePollingRequest(clientETag);
}
// if the If-None-Match header was not supplied then
// this is the first request for the resource.
// set an Accepted response message.
var response = new HttpResponseMessage(HttpStatusCode.Accepted);
// set a weak ETag.
var requestUri = this.Request.RequestUri.ToString();
var serverETag = ETag.Create(requestUri);
response.Headers.ETag = new EntityTagHeaderValue(serverETag, true);
// set a "retry after" suggestion.
response.Headers.RetryAfter = new RetryConditionHeaderValue(new TimeSpan(0, 0, 10));
this.cache.StringSet(serverETag, requestUri);
var myRequestDto = this.mapper.Map<PlanRequestDto>(myRequest);
myRequestDto.ETag = serverETag;
// put the work on the queue.
this.myRequestQueue.Client.SendAsync(
new BrokeredMessage(myRequestDto));
return response;
}
private HttpResponseMessage HandlePollingRequest(EntityTagHeaderValue clientETag)
{
HttpResponseMessage response;
var cachedValue = this.cache.StringGet(clientETag.Tag);
if (cachedValue == null)
{
// cache may have expired or
// the client may have an incorrect ETag.
return
this.Request.CreateResponse(
HttpStatusCode.PreconditionFailed,
Errors.NoETag);
}
// get the hash of the value in the cache.
var serverETag = ETag.Create(cachedValue);
// check the request ETag against the value in the cache
// to see if the resource has been updated.
if (clientETag.Tag.Equals(serverETag))
{
// if they match then the resource isn't available yet so
// return 304 (Not-Modified).
response = new HttpResponseMessage(HttpStatusCode.NotModified);
// add the ETag for the next polling request to use.
response.Headers.ETag = clientETag;
// set a retry after suggestion.
response.Headers.RetryAfter = new RetryConditionHeaderValue(new TimeSpan(0, 0, 10));
}
else
{
var myResponseDto = new JavaScriptSerializer().Deserialise<MyResponseDto>(cachedValue);
var myResponse = this.mapper.Map<MyResponse>(myResponseDto);
// the resource is available now so
// the updated resource should be returned.
response = this.Request.CreateResponse(HttpStatusCode.OK, myResponse);
// set the ETag for completeness.
response.Headers.ETag = clientETag;
}
return response;
}
}
public static class ETag
{
public static string Create(string cachedValue)
{
byte[] bytes = Encoding.ASCII.GetBytes(cachedValue);
byte[] hash = SHA256.Create().ComputeHash(bytes);
// ETag must be quoted string.
var builder = new StringBuilder("\"");
foreach (byte t in hash)
{
builder.Append(t.ToString("x2"));
}
builder.Append("\"");
string serverETag = builder.ToString();
return serverETag;
}
}
}
namespace MyConsoleApplicationAsync
{
public class Program
{
public static void Main(string[] args)
{
if (args.Length < 2)
{
ShowUsage();
return;
}
MainAsync(args).Wait();
}
private static async Task MainAsync(string[] args)
{
var baseAddress = args[0];
var apiRoute = args[1];
var querystring = args[2];
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(baseAddress);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
// request with querystring.
var response = await client.GetAsync(apiRoute + querystring);
// request has been accepted (the first response) or
// response is not yet available in the server's cache.
while (response.StatusCode.Equals(HttpStatusCode.Accepted) ||
response.StatusCode.Equals(HttpStatusCode.NotModified))
{
// use the suggested timeout from the server.
var retryAfter = response.Headers.RetryAfter.Delta;
if (retryAfter.HasValue)
{
Thread.Sleep(retryAfter.Value);
}
// set the ETag (EntityTag) to check in the server's cache.
client.DefaultRequestHeaders.IfNoneMatch.Clear();
client.DefaultRequestHeaders.IfNoneMatch.Add(
response.Headers.ETag);
// check cache.
response = await client.GetAsync(new Uri(apiRoute));
}
// should be 200 (OK) but may be an error code.
Console.WriteLine(response.StatusCode);
Console.WriteLine(response.ReasonPhrase);
Console.WriteLine(response.Content.ReadAsStringAsync());
}
}
private static void ShowUsage()
{
var usage = new StringBuilder("Call the Web API.");
usage.AppendLine("Usage:");
usage.AppendLine("myApi baseAddress apiRoute querystring");
usage.AppendLine("Example:");
usage.AppendLine("myApi 'http://localhost:8080/' 'api/my' '?age=42&postcode=W1A4WW'");
Console.WriteLine(usage.ToString());
}
}
}
namespace WorkerRole
{
public class WorkerRole : RoleEntryPoint
{
private readonly IMyResponseQueue responseQueue;
private readonly ICache cache;
public WorkerRole(ICache cache, IMyResponseQueue responseQueue)
{
this.cache = cache;
this.responseQueue = responseQueue;
}
public override void Run()
{
this.responseQueue.Client.OnMessageAsync(
async msg =>
{
this.ProcessMessage(msg);
});
}
private async Task ProcessMessage(BrokeredMessage message)
{
var myResponseDto = message.GetBody<MyResponseDto>();
var serialisedMyResponseDto = new JavaScriptSerializer().Serialise(myResponseDto);
this.cache.StringSet(myResponseDto.ETag, serialisedMyResponseDto);
message.CompleteAsync();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment