C# Client Library
A C# Client Library for the AnalyzeRe REST API
Loading...
Searching...
No Matches
API.Utilities.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Concurrent;
3using System.Collections.Generic;
4using System.Diagnostics;
5using System.Globalization;
6using System.Linq;
7using System.Net;
8using System.Reflection;
9using System.Text;
10using System.Threading;
11using System.Threading.Tasks;
12
14using AnalyzeRe.Rest;
16using Newtonsoft.Json;
17using RestSharp;
18
19namespace AnalyzeRe
20{
22 public static partial class API
23 {
24 internal const string DebugTimeFormat = @"yyyy-MM-dd HH:mm:ss.FFFF";
25 private const NumberStyles DoubleParseStyle = NumberStyles.AllowDecimalPoint;
26 private const NumberStyles IntParseStyle = NumberStyles.None;
27 private static readonly CultureInfo ParseCulture = CultureInfo.InvariantCulture;
28
29 #region Public Utilities
34 public static bool IsAReServerActive(string checkServerURL, int? timeout = null)
35 {
36 try
37 {
38 RestClient testClient = GetRestClient(new Uri(checkServerURL));
40 {
42 };
44 // If the request gets through and the server header identifies the response
45 // as coming from an Analyze Re API server, return true.
46 if (response.ResponseStatus == ResponseStatus.Completed &&
48 response.Headers.Any(h => h.Name == "ARE-Server")))
49 {
50 return true;
51 }
52 }
53 // ReSharper disable once EmptyGeneralCatchClause (any error means URL is not valid)
54 catch { }
55 return false;
56 }
57
58 #region Polling
77 {
79 int count = 0, sleep_ms = 0, elapsed_ms, remaining_ms;
80 DateTime start = DateTime.UtcNow;
81 bool completed;
83
84 do
85 {
86 if (pollingOptions.CancellationToken?.IsCancellationRequested ?? false)
87 throw new TaskCanceledException();
88 if (sleep_ms != 0)
89 Thread.Sleep(sleep_ms);
90 // Invoke the supplied methods
91 response = request();
93 // Increment the various counters and prepare the next sleep time.
94 count++;
95 elapsed_ms = (int)(DateTime.UtcNow - start).TotalMilliseconds;
96 remaining_ms = pollingOptions.MaxPollTotalTime - elapsed_ms;
97 sleep_ms = pollingOptions.GetNextPollTime(sleep_ms, elapsed_ms);
98 // Loop until we time out or the status is finalized
99 } while (!completed && remaining_ms > 0);
100
101 if (!completed)
102 throw new NotWaitingException(
103 $"The request is still not complete after {elapsed_ms / 1000d} seconds, which " +
104 $"exceeds the timeout of {pollingOptions.MaxPollTotalTime / 1000d}s.",
105 elapsed_ms / 1000d, maxWaitTime: pollingOptions.MaxPollTotalTime / 1000d,
107
108 Debug.WriteLine($"Polling completed after {count} attempts (took {elapsed_ms / 1000d} seconds).");
109 return response;
110 }
111
121 => PollUntil(request, result => result, pollingOptions);
122
138 public static T PollUntilReady<T>(
141 {
142 double? suggestedWaitSeconds = null;
143 int? queuePosition = null;
144 Exception handled = null;
145 try
146 {
147 object PollingRequest()
148 {
149 try
150 {
152 queuePosition = null;
154 if (response == null)
155 throw new Exception("The server replied successfully, but the response was null.");
156 return response;
157 }
158 // APIRequestException is expected so long as the result is not yet ready.
159 // TODO: Encapsulate this exception parsing logic so anyone can benefit
160 catch (APIRequestException ex)
161 {
162 if (ex.RestResponse?.StatusCode != HttpStatusCode.ServiceUnavailable)
163 throw;
164
165 // Parse the retryAfter header out of the response.
166 Parameter retryAfter = ex.RestResponse.Headers.FirstOrDefault(
167 header => header.Name.Equals("Retry-After", StringComparison.OrdinalIgnoreCase));
168 if (retryAfter == null)
170 else if (Double.TryParse(retryAfter.Value.ToString(),
171 DoubleParseStyle, ParseCulture, out double retryAfterParsed))
173 else
174 throw new APIRequestException("The server provided a Retry-After header, " +
175 $"but it could not be parsed: {retryAfter.Value}",
176 ex, ex.RestResponse, ex.ServerError);
177
178 // Parse the queuePosition header out of the response.
179 Parameter queueHeader = ex.RestResponse.Headers.FirstOrDefault(
180 header => header.Name.Equals("Queue-Position", StringComparison.OrdinalIgnoreCase));
181 if (queueHeader == null)
182 queuePosition = null;
183 else if (Int32.TryParse(queueHeader.Value.ToString(),
184 IntParseStyle, ParseCulture, out int queuePositionParsed))
186 else
187 throw new APIRequestException("The server provided a Queue-Position header, " +
188 $"but it could not be parsed: {queueHeader.Value}",
189 ex, ex.RestResponse, ex.ServerError);
190
191 handled = ex;
192 }
193 return null;
194 }
195
197 isPollingComplete: res => res != null,
199 }
200 // Intercept the NotWaitingException and re-raise with a more meaningful message.
201 catch (NotWaitingException ex)
202 {
203 string suffix = ((ex.MaxWaitTime ?? 0d) <= 0d ? "" :
204 $"The maximum wait time of {ex.MaxWaitTime} seconds has been exceeded. ") +
205 "Please try again later.";
206 string message = null;
207 // If the server is returning a queue position or ETA, include it in the message
208 if (queuePosition != null)
209 message += $"Simulation is queued at position {queuePosition}. ";
210 if (suggestedWaitSeconds != null)
211 message += $"Server suggests retrying after {suggestedWaitSeconds} seconds. ";
212 // Raise a new NotWaitingException with a custom message
213 throw new NotWaitingException(message + suffix,
214 ex.TimeWaited,
217 maxWaitTime: ex.MaxWaitTime,
220 requestEx.RestResponse : ex.RestResponse);
221 }
222 }
223 #endregion Polling
224
225 #region Get Collection Name
226 #region Method Overloads
233 public static string GetCollectionName<T>() where T : IAPIResource
234 {
235 return GetCollectionName<T>(out Type _);
236 }
249
256 public static string GetCollectionName(Type requestType)
257 {
258 return GetCollectionName(requestType, out Type _);
259 }
260 #endregion Method Overloads
261
264 CollectionNameCache = new ConcurrentDictionary<Type, Tuple<Type, string>>();
265
277 public static string GetCollectionName(Type requestType, out Type declaringClassType,
278 bool suppress_throw = false)
279 {
280 // Standard resource types as a rule have this static field defined in each resource class.
281 const string collectionNameField = "CLASS_COLLECTION_NAME";
282 // Any class that implements IAPIResourceView must have this public gettable property
283 // that returns the resource collection name
284 const string interfaceCollectionProperty = "collection_name";
285
286 // Check if we've already looked up the collection name for this type
287 if (CollectionNameCache.TryGetValue(requestType, out Tuple<Type, string> cachedEntry))
288 {
289 // If we have a cached entry that is null, it means we've previously gone through
290 // the work of determining that this is an unsupported type.
291 // Reuse that knowledge if suppress_throw is set and return null, otherwise
292 // if the value is null, go through the work again to throw a meaningful exception.
293 if (cachedEntry != null || suppress_throw)
294 {
296 return cachedEntry?.Item2;
297 }
298 }
299 // If the entry is not in the cache, we must compute it.
300
301 // First, throw an error if this isn't a supported type (only resources have collections)
303 {
304 if (!suppress_throw)
305 throw new ArgumentException($"{requestType.NiceTypeName()} " +
306 "does not derive from IAPIResource.");
307 declaringClassType = null;
308 return null;
309 }
310
311 // Binding flags combinations that get used a few times
313 BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Static;
315 BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance;
316
317 string collectionName = null;
318 // For a start, assume the requestType is the base resource type
320
321 // Hack: If the declaring type is generic, the field is re-defined in each generic type.
322 // We need to find the base covariant type which defines this.
323 if (declaringClassType.IsGenericType)
324 {
325 declaringClassType = declaringClassType.GetGenericTypeDefinition();
326 // Assume there's only one parameter and fill in from the type constraint.
327 declaringClassType = declaringClassType.MakeGenericTypeFast(
328 declaringClassType.GetGenericArguments()[0].GetGenericParameterConstraints()[0]);
329 }
330
331 // Most classes define a public static CLASS_COLLECTION_NAME field,
332 // which is simplest to use, but this is not a necessary interface member.
334 if (field != null)
335 {
336 declaringClassType = field.DeclaringType;
337 collectionName = (string)field.GetValue(null);
338 }
339 // If they didn't follow the CLASS_COLLECTION_NAME convention, then an instance of the type
340 // must at least have a collection_name property, according to the IApiResource interface
341 else if (!declaringClassType.IsAbstract && !declaringClassType.IsInterface)
342 {
343 // See if we can get the collection name by creating an instance of the declaring class type
344 try
345 {
349 // This should never happen if this requestType is an IAPIResource
350 if (collectionNamePropertyInfo == null)
351 throw new Exception("The type " + requestType.NiceTypeName() +
352 " does not define or inherit a collection_name property.");
353 // Get the base class that actually declared this property
356 }
357 catch { collectionName = null; }
358 }
359
360 // If we don't have collectionName yet we have a non-instantiable base class or
361 // interface type on our hands. We can try to search for a derived type that does
362 // implement one of these properties, but it might break covariance.
363 if (collectionName == null)
364 {
365 // Hack: The IAPIResource interface does enforce the existence of a collection_name
366 // instance property, it just can't declare it, so we'll have to get an
367 // instance of this type (or a sub-type) to get the collection name, but this
368 // won't necessarily be the declaring type.
370
371 // Try to get the collection name from each of these instantiable types.
372 // If all that supply the collection name are in agreement about the collection
373 // name and declaringClassType, then return those as the result.
374 foreach (Type instantiable in subTypes)
375 {
376 string tempCollectionName = null;
377 Type tempDeclaringClassType = null;
378 try
379 {
381 }
382 // ReSharper disable once EmptyGeneralCatchClause
383 // (Don't care about errors in recursive calls.)
384 catch { }
385 // If this instantiable type didn't have this property, continue
386 if (tempCollectionName == null) continue;
387 // If this is the first result we've received, store the results
388 if (collectionName == null)
389 {
392 }
393 // Otherwise, ensure the new result agrees with the prior results.
394 // This verifies that derived objects report the same collection name
396 {
397 // Cache the null result and either raise an error or allow null to be returned
398 CollectionNameCache.TryAdd(requestType, null);
399 if (!suppress_throw)
400 throw new ArgumentException("Invalid type " + requestType.NiceTypeName() +
401 ". Type can be linked to more than one resource (" + collectionName +
402 ", " + tempCollectionName + ")");
403 declaringClassType = null;
404 return null;
405 }
406 // This verifies that all derived objects report the base same class
408 {
409 CollectionNameCache.TryAdd(requestType, null);
410 if (!suppress_throw)
411 throw new Exception("Error determining the bass class for the collection " +
412 collectionName + " from the type " + requestType.NiceTypeName() +
413 ". Collection declared by more than one Type (" + declaringClassType.NiceTypeName() +
414 ", " + tempDeclaringClassType.NiceTypeName() + ")");
415 declaringClassType = null;
416 return null;
417 }
418 }
419 }
420 // Cache this result so we never have to go to all this work again!
421 if (collectionName != null)
422 {
423 CollectionNameCache.TryAdd(requestType,
425 }
427 return cachedEntry?.Item2;
428 }
429
436 public static string GetCollectionNameFromResourceHref(string href)
437 {
438 // Expect the collection to be in the second last segment of the reference URL.
439 int guid_start = href.LastIndexOf('/', href.Length - 2);
440 int collection_start = href.LastIndexOf('/', guid_start - 1) + 1;
441 return href.Substring(collection_start, guid_start - collection_start);
442 }
443 #endregion Get Collection Name
444 #endregion Public Utilities
445
446 #region Internal Utilities / Helper Methods
447 internal const string ERR_MISSING_ID =
448 "The specified resource's 'id' property is missing. " +
449 "Did you forget to POST this resource and store the result? For example:\n" +
450 "var saved_resource = staged_resource.Post();";
451
458 internal static string GetResourcePath(IAPIResource resource)
459 {
460 ValidateExistingResource(resource);
461 return $"{resource.collection_name}/{resource.id}";
462 }
463
469 internal static string GetCollectionPath(IAPIResource resource)
470 {
471 if (resource == null)
473 return $"{resource.collection_name}/";
474 }
475
480 internal static RequestParameters AddRequestBodyParameters(
482 {
484 .AddParameter(JSON_CONTENT_TYPE, request_body, ParameterType.RequestBody);
485 }
486
492 internal static void ValidateExistingResource(IAPIResource resource)
493 {
494 if (resource == null)
496 ValidateResourceId(resource.id);
497 }
498
503 internal static void ValidateResourceId(string id)
504 {
505 if (String.IsNullOrEmpty(id))
506 throw new ArgumentException(ERR_MISSING_ID);
507 // TODO: Perhaps it is worthwhile to verify that the id is a valid GUID
508 }
509 #endregion Internal Utilities / Helper Methods
510
511 #region Private Utilities / Helper Methods
514 private static APIRequestException ServerErrorExceptionHelper(
515 IRestResponse response = null, Exception inner = null,
516 ServerError serverErr = null, string message = null)
517 {
518 if (message != null)
519 return new APIRequestException(message, inner, response, serverErr);
520 if (serverErr?.message != null)
521 {
522 message = (response != null ? "Server threw " + response.StatusCode
523 : "Server error") + ": " + serverErr.message.TrimEnd('.');
524 }
525 else if (response != null)
526 {
527 if (inner != null)
528 {
529 message = "Server response content error: " + inner.Message.TrimEnd('.') +
530 ". Response Content: " + TryFormat(response.Content) +
531 "\n See inner exception and restResponse properties for details.";
532 }
533 else
534 {
535 message = "Server response error. Response Status: " +
536 response.StatusCode + ", Content: " + TryFormat(response.Content) +
537 ". See restResponse property for details.";
538 }
539 }
540 else if (inner != null)
541 {
542 message = "Exception encountered: " + inner.Message.TrimEnd('.') +
543 ". See inner exception for details.";
544 }
545 return new APIRequestException(message, inner, response, serverErr);
546 }
547
548 #region Debug Helper Methods
551 private static void DebugRestRequest(IRestRequest request)
552 {
553 Debug.Write(request.PrettyPrint() + "\n");
554 }
555
558 private static void DebugRestResponse(IRestResponse response)
559 {
560 Debug.Write("\n" + response.PrettyPrint() + "\n");
561 }
562
563 #region Debug Printing Extension Methods
568 public static string PrettyPrint(this IRestRequest request)
569 {
570 const int indent = 2;
571 string pad = new string(' ', indent);
572 StringBuilder result = new StringBuilder();
573
574 result.AppendLine("Processing the following request: ");
575 result.AppendLine($"{pad}Current Time: {DateTime.UtcNow.ToString(DebugTimeFormat)}");
576 result.AppendLine($"{pad}Request Method: {request.Method}");
577 result.AppendLine($"{pad}Request Resource: " +
578 (String.IsNullOrWhiteSpace(request.Resource) ? "(root)" : request.Resource));
579 // Separate request parameters by type and write them out as a list.
580 result.Append(String.Join("", request.Parameters.OrderByDescending(p => p.Type)
581 .ThenBy(p => p.Name).Where(p => p.Type != ParameterType.RequestBody)
582 .GroupBy(p => p.Type).Select(g => $"{pad}{g.Key} Params:".PadRight(20 + indent) +
583 String.Join("\n" + new string(' ', 20 + indent),
584 g.Select(px => $"{px.Name} = \"{px.Value}\"")) + "\n")));
585 Parameter body = request.Parameters.FirstOrDefault(
586 p => p.Type == ParameterType.RequestBody);
587 if (body != null)
588 {
589 result.AppendLine(pad + "Request Body (" + body.Name + "): " +
590 TryFormat(body.Value, body.Name == JSON_CONTENT_TYPE, indent, true));
591 }
592 else
593 result.Append(pad + "Request Body: (null)");
594 return result.ToString();
595 }
596
601 public static string PrettyPrint(this IRestResponse response)
602 {
603 const int indent = 2;
604 string pad = new string(' ', indent);
605 StringBuilder result = new StringBuilder();
606
607 if (response.ResponseStatus == ResponseStatus.Completed)
608 result.AppendLine("The following response was received:");
609 else
610 result.AppendLine("Error while receiving a response from the server:");
611 result.AppendLine($"{pad}Current Time: {DateTime.UtcNow.ToString(DebugTimeFormat)}");
612 if (response.ResponseStatus != ResponseStatus.Completed)
613 {
614 result.AppendLine($"{pad}Response Status: {response.ResponseStatus}");
615 result.Append($"{pad}Response Error: {response.ErrorMessage}");
616 // None of the standard response logs are available if this is the case.
617 return result.ToString();
618 }
619 // Avoid polluting logs with lots of RetryAfter logs by keeping them lean.
620 if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
621 {
622 Parameter retryHeader = response.Headers.FirstOrDefault(header =>
623 header.Name.Equals("Retry-After", StringComparison.OrdinalIgnoreCase));
624 Parameter queuePosition = response.Headers.FirstOrDefault(header =>
625 header.Name.Equals("Queue-Position", StringComparison.OrdinalIgnoreCase));
626 result.Append(pad + "Received '503 - Service Unavailable' with " +
627 (retryHeader == null ? "no Retry-After header" : $"Retry-After: {retryHeader.Value}") +
628 (queuePosition == null ? " and no Queue-Position header" : $" and Queue-Position: {queuePosition.Value}"));
629 return result.ToString();
630 }
631 // Standard Response Logging
632 result.AppendLine($"{pad}Response URI: {response.ResponseUri}");
633 result.AppendLine($"{pad}Response HTTP Code: " +
634 $"({(int)response.StatusCode}) {response.StatusDescription}");
635 result.AppendLine($"{pad}Response Headers: " +
636 String.Join("\n" + new string(' ', 20 + indent),
637 response.Headers.Select(p => $"{p.Name}: \"{p.Value}\"").OrderBy(s => s)));
638 string contentType = String.IsNullOrWhiteSpace(response.ContentType) ?
639 null : $" ({response.ContentType})";
640 result.Append(pad + $"Response Body{contentType}: ".PadRight(20) +
641 TryFormat(response.Content, contentType?.Contains("json") ?? false, indent, true));
642 return result.ToString();
643 }
644
649 public static bool IsSuccessful(this IRestResponse response)
650 {
651 return response != null && response.ResponseStatus == ResponseStatus.Completed &&
652 (((int)response.StatusCode) >= 200 && ((int)response.StatusCode) < 300);
653 }
654 #endregion Debug Printing Extension Methods
655
664 private static string TryFormat(object content, bool tryJSON = false, int indent = 0,
665 bool newLineIfMultiLine = false)
666 {
667 if (content == null) return "(null)";
668 string retVal = content.ToString();
669 if (String.IsNullOrWhiteSpace(retVal))
670 return "(response content was either empty or got redirected to a user-defined stream)";
671 if (retVal.Length >= 10000)
672 return $"{retVal.Substring(0, 1000)}... " +
673 $"(response content is too long to display in full: {retVal.Length} characters)";
674 // Often the server response is application/json, but with a value of "null", so avoid
675 // that common exception by skipping this if that's the case.
676 if (tryJSON && retVal != "null")
677 {
678 try
679 {
681 {
682 DateParseHandling = DateParseHandling.None
683 };
684 retVal = JsonConvert.DeserializeObject(retVal, s).ToString();
685 }
686 // ReSharper disable once EmptyGeneralCatchClause (we don't care if it's not JSON)
687 catch { }
688 }
689 // If this is a multi-line message, put it on it's own new line.
690 if (newLineIfMultiLine && retVal.Contains("\n"))
691 retVal = "\n" + retVal;
692 // Add indenting if applicable.
693 if (indent > 0)
694 retVal = retVal.Replace("\n", "\n" + new string(' ', indent));
695 return retVal;
696 }
697 #endregion Debug Helper Methods
698 #endregion Private Utilities / Helper Methods
699
700 #region Deprecated Methods
723 [Obsolete("This method has been replaced with an overload that uses a PollingOptions object")]
724 public static T PollUntilReady<T>(
726 double minPollInterval = 0d,
727 double maxPollInterval = Double.MaxValue,
728 double maxPollTotalTime = Double.MaxValue)
729 {
731 (int)minPollInterval * 1000, (int)maxPollInterval * 1000, (int)maxPollTotalTime * 1000));
732 }
733 #endregion Deprecated Methods
734 }
735}
A custom exception class that includes the RestSharp.IRestResponse that generated the exception,...
Describes a collection of resources which can be listed.
Used to deserialize server error messages.
Definition ServerError.cs:9
API methods / requests made available to the user.
static string PrettyPrint(this IRestRequest request)
Extension method that generates a helpful string representation of an IRestRequest.
static int DefaultRequestTimeout
The default timeout used for all simple server requests, in milliseconds.
static bool IsAReServerActive(string checkServerURL, int? timeout=null)
Determines if the provided URL is a valid running Analyze Re server.
static string GetCollectionName< T >()
Gets the collection name for the given APIResource type, whether it's instantiable or not.
static bool IsSuccessful(this IRestResponse response)
Extension method that returns true if the restResponse was completed and contains a successful status...
static void PollUntil(Func< bool > request, AnalyzeRe.PollingOptions pollingOptions=null)
Poll the specified request until it returns true.
static string PrettyPrint(this IRestResponse response)
Extension method that generates a helpful string representation of an IRestResponse.
static string GetCollectionName(Type requestType, out Type declaringClassType, bool suppress_throw=false)
Gets the collection name for the given APIResource type, whether it's instantiable or not.
static T PollUntilReady< T >(Func< T > functionThatMight503, AnalyzeRe.PollingOptions pollingOptions=null)
Specialized polling that will executes the specified request delegate, but if a 503 retry-after APIRe...
static readonly string AnalyzeReServerIdentity
The Server identity returned by valid AnalyzeRe servers.
static string GetCollectionName(Type requestType)
Gets the collection name for the given APIResource type, whether it's instantiable or not.
static TResponse PollUntil< TResponse >(Func< TResponse > request, Func< TResponse, bool > isPollingComplete, AnalyzeRe.PollingOptions pollingOptions=null)
Poll the requested resource until a desired state is attained.
static string GetCollectionNameFromResourceHref(string href)
Return the collection name implied by the href for a given resource assuming the URL is in the format...
Thrown when a request requires additional time to complete, but it exceeds the time we are willing to...
Determines the behaviour of the API when automatically retrying a request whose result is not yet rea...
static PollingOptions Default
The default PollingArguments used when none are specified.
Helper class which makes it easier to build a set of request parameters.
RequestParameters AddParameter(Parameter parameter)
Adds the specified parameter to this list and returns the list.
Utility for resolving types given only the types name (Useful for parsing ambiguous JSON objects).
static List< Type > GetAllInstantiableSubtypes(Type type)
Attempts to create a list of types in the current application domain's assemblies derived from the gi...
Interface for Base class used by all resources.