1+ using Newtonsoft . Json ;
2+ using System ;
3+ using System . Net ;
4+ using System . Net . Http ;
5+ using System . Net . Http . Headers ;
6+ using System . Text ;
7+ using System . Threading ;
8+ using System . Threading . Tasks ;
9+
10+ namespace Rubberduck . Client . Abstract
11+ {
12+ public abstract class ApiClientBase : IDisposable
13+ {
14+ protected static readonly string UserAgentName = "Rubberduck" ;
15+ protected static readonly string BaseUrl = "https://api.rubberduckvba.com/api/v1/" ;
16+ protected static readonly string ContentTypeApplicationJson = "application/json" ;
17+ protected static readonly int MaxAttempts = 3 ;
18+ protected static readonly TimeSpan RetryCooldownDelay = TimeSpan . FromSeconds ( 1 ) ;
19+
20+ protected readonly Lazy < HttpClient > _client ;
21+
22+ protected ApiClientBase ( )
23+ {
24+ _client = new Lazy < HttpClient > ( ( ) => GetClient ( ) ) ;
25+ }
26+
27+ protected HttpClient GetClient ( )
28+ {
29+ ServicePointManager . SecurityProtocol = SecurityProtocolType . Tls12 ;
30+ var client = new HttpClient ( ) ;
31+ return ConfigureClient ( client ) ;
32+ }
33+
34+ protected virtual HttpClient ConfigureClient ( HttpClient client )
35+ {
36+ var userAgentVersion = System . Reflection . Assembly . GetExecutingAssembly ( ) . GetName ( ) . Version . ToString ( 3 ) ;
37+ var userAgentHeader = new ProductInfoHeaderValue ( UserAgentName , userAgentVersion ) ;
38+
39+ client . DefaultRequestHeaders . UserAgent . Add ( userAgentHeader ) ;
40+ return client ;
41+ }
42+
43+ protected virtual async Task < TResult > GetResponse < TResult > ( string route , CancellationToken ? cancellationToken = null )
44+ {
45+ var uri = new Uri ( $ "{ BaseUrl } { route } ") ;
46+
47+ var attempt = 0 ;
48+ var token = cancellationToken ?? CancellationToken . None ;
49+
50+ while ( ! token . IsCancellationRequested && attempt <= MaxAttempts )
51+ {
52+ attempt ++ ;
53+ var delay = attempt == 0 ? TimeSpan . Zero : RetryCooldownDelay ;
54+
55+ var ( success , result ) = await TryGetResponse < TResult > ( uri , delay , token ) ;
56+ if ( success )
57+ {
58+ return result ;
59+ }
60+ }
61+
62+ token . ThrowIfCancellationRequested ( ) ;
63+ throw new InvalidOperationException ( $ "API call failed to return a result after { attempt } attempts.") ;
64+ }
65+
66+ private async Task < ( bool , TResult ) > TryGetResponse < TResult > ( Uri uri , TimeSpan delay , CancellationToken token )
67+ {
68+ if ( delay != TimeSpan . Zero )
69+ {
70+ await Task . Delay ( delay ) ;
71+ }
72+
73+ token . ThrowIfCancellationRequested ( ) ;
74+
75+ try
76+ {
77+ using ( var client = GetClient ( ) )
78+ {
79+ using ( var response = await client . GetAsync ( uri ) )
80+ {
81+ response . EnsureSuccessStatusCode ( ) ;
82+ token . ThrowIfCancellationRequested ( ) ;
83+
84+ var content = await response . Content . ReadAsStringAsync ( ) ;
85+ var result = JsonConvert . DeserializeObject < TResult > ( content ) ;
86+
87+ return ( true , result ) ;
88+ }
89+ }
90+ }
91+ catch ( OperationCanceledException )
92+ {
93+ throw ;
94+ }
95+ catch
96+ {
97+ return default ;
98+ }
99+ }
100+
101+ protected virtual async Task < T > Post < T > ( string route , T args , CancellationToken ? cancellationToken = null ) => await Post < T , T > ( route , args , cancellationToken ?? CancellationToken . None ) ;
102+
103+ protected virtual async Task < TResult > Post < TArgs , TResult > ( string route , TArgs args , CancellationToken ? cancellationToken = null )
104+ {
105+ var uri = new Uri ( $ "{ BaseUrl } { route } ") ;
106+ string json ;
107+ try
108+ {
109+ json = JsonConvert . SerializeObject ( args ) ;
110+ }
111+ catch ( Exception exception )
112+ {
113+ throw new ArgumentException ( "The specified arguments could not be serialized." , exception ) ;
114+ }
115+
116+ var attempt = 0 ;
117+ var token = cancellationToken ?? CancellationToken . None ;
118+
119+ while ( ! token . IsCancellationRequested && attempt <= MaxAttempts )
120+ {
121+ attempt ++ ;
122+ var delay = attempt == 0 ? TimeSpan . Zero : RetryCooldownDelay ;
123+
124+ var ( success , result ) = await TryPost < TResult > ( uri , json , delay , token ) ;
125+ if ( success )
126+ {
127+ return result ;
128+ }
129+ }
130+
131+ token . ThrowIfCancellationRequested ( ) ;
132+ throw new InvalidOperationException ( $ "API call failed to return a result after { attempt } attempts.") ;
133+ }
134+
135+ private async Task < ( bool , TResult ) > TryPost < TResult > ( Uri uri , string body , TimeSpan delay , CancellationToken token )
136+ {
137+ if ( delay != TimeSpan . Zero )
138+ {
139+ await Task . Delay ( delay ) ;
140+ }
141+
142+ token . ThrowIfCancellationRequested ( ) ;
143+
144+ try
145+ {
146+ using ( var client = GetClient ( ) )
147+ {
148+ var content = new StringContent ( body , Encoding . UTF8 , ContentTypeApplicationJson ) ;
149+ using ( var response = await client . PostAsync ( uri , content , token ) )
150+ {
151+ response . EnsureSuccessStatusCode ( ) ;
152+ token . ThrowIfCancellationRequested ( ) ;
153+
154+ var jsonResult = await response . Content . ReadAsStringAsync ( ) ;
155+ var result = JsonConvert . DeserializeObject < TResult > ( jsonResult ) ;
156+
157+ return ( true , result ) ;
158+ }
159+ }
160+ }
161+ catch ( OperationCanceledException )
162+ {
163+ throw ;
164+ }
165+ catch
166+ {
167+ return default ;
168+ }
169+ }
170+
171+ public void Dispose ( )
172+ {
173+ if ( _client . IsValueCreated )
174+ {
175+ _client . Value . Dispose ( ) ;
176+ }
177+ }
178+ }
179+ }
0 commit comments