diff --git a/ButterCMS/ButterCMS.csproj b/ButterCMS/ButterCMS.csproj index d1fdedf..aae1ef0 100644 --- a/ButterCMS/ButterCMS.csproj +++ b/ButterCMS/ButterCMS.csproj @@ -60,6 +60,9 @@ + + + diff --git a/ButterCMS/ButterCMSClient.cs b/ButterCMS/ButterCMSClient.cs index 38350e3..27c1f43 100644 --- a/ButterCMS/ButterCMSClient.cs +++ b/ButterCMS/ButterCMSClient.cs @@ -12,9 +12,12 @@ namespace ButterCMS { public class ButterCMSClient { + private static readonly string generalExMsg = "There is a problem with the ButterCMS service"; + private string authToken; private HttpClient httpClient; private TimeSpan defaultTimeout = new TimeSpan(0, 0, 10); + private int maxRequestTries; private const string apiBaseAddress = "https://api.buttercms.com/"; @@ -41,13 +44,14 @@ private string authTokenParam } private JsonSerializerSettings serializerSettings; - public ButterCMSClient(string authToken, TimeSpan? timeOut = null) + public ButterCMSClient(string authToken, TimeSpan? timeOut = null, int maxRequestTries = 3) { httpClient = new HttpClient { Timeout = timeOut ?? defaultTimeout, BaseAddress = new Uri(apiBaseAddress) }; + this.maxRequestTries = maxRequestTries; this.authToken = authToken; serializerSettings = new JsonSerializerSettings(); @@ -453,8 +457,30 @@ public async Task RetrieveContentFieldsJSONAsync(string[] keys) var contentFields = await ExecuteAsync(queryString); return contentFields; } - + private string Execute(string queryString) + { + var remainingTries = maxRequestTries; + var exceptions = new List(); + + do + { + --remainingTries; + try + { + return ExecuteSingle(queryString); + } + catch (Exception e) + { + exceptions.Add(e); + } + } + while (remainingTries > 0); + + throw aggregateExceptions(exceptions); + } + + private string ExecuteSingle(string queryString) { try { @@ -469,7 +495,7 @@ private string Execute(string queryString) } if (response.StatusCode >= System.Net.HttpStatusCode.InternalServerError) { - throw new Exception("There is a problem with the ButterCMS service"); + throw new Exception(generalExMsg); } } catch (TaskCanceledException taskException) @@ -492,6 +518,28 @@ private string Execute(string queryString) } private async Task ExecuteAsync(string queryString) + { + var remainingTries = maxRequestTries; + var exceptions = new List(); + + do + { + --remainingTries; + try + { + return await ExecuteSingleAsync(queryString); + } + catch (Exception e) + { + exceptions.Add(e); + } + } + while (remainingTries > 0); + + throw aggregateExceptions(exceptions); + } + + private async Task ExecuteSingleAsync(string queryString) { try { @@ -506,7 +554,7 @@ private async Task ExecuteAsync(string queryString) } if (response.StatusCode >= System.Net.HttpStatusCode.InternalServerError) { - throw new Exception("There is a problem with the ButterCMS service"); + throw new Exception(generalExMsg); } } catch (TaskCanceledException taskException) @@ -527,5 +575,55 @@ private async Task ExecuteAsync(string queryString) } return string.Empty; } + + private Exception aggregateExceptions(List exceptions) + { + // If we somehow managed to fail all the requests without getting any exceptions, + // return a general exception. This shouldn't be possible. + if (!exceptions.Any()) + return new Exception(generalExMsg); + + var uniqueExceptions = exceptions.Distinct(new ExceptionEqualityComparer()); + + // If all the requests failed with the same exception (should be the case most of the time), + // just return one exception to represent them all. + if (uniqueExceptions.Count() == 1) + return uniqueExceptions.First(); + + // If all the requests failed but for different reasons, return an AggregateException + // with all the root-cause exceptions. + return new AggregateException(generalExMsg, uniqueExceptions); + } + + /// + /// Used to aggregate exceptions that occur on request retries. + /// + /// + /// In most cases, the same exception will occur multiple times, + /// but we don't want to return multiple copies of it. This class is used + /// to find exceptions that are duplicates by type and message so we can + /// only return one of them. + /// + private class ExceptionEqualityComparer : IEqualityComparer + { + public bool Equals(Exception e1, Exception e2) + { + if (e2 == null && e1 == null) + return true; + else if (e1 == null | e2 == null) + return false; + else if (e1.GetType().Name.Equals(e2.GetType().Name) && e1.Message.Equals(e2.Message)) + return true; + else + return false; + } + + public int GetHashCode(Exception e) + { + return (e.GetType().Name + e.Message).GetHashCode(); + } + } + } + }