• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rwjdk / TrelloDotNet / 24049362947

06 Apr 2026 08:19PM UTC coverage: 78.209% (-0.03%) from 78.241%
24049362947

push

github

web-flow
Add Udemy Course link to README (#66)

2637 of 3638 branches covered (72.48%)

Branch coverage included in aggregate %.

4681 of 5719 relevant lines covered (81.85%)

146.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.96
/src/TrelloDotNet/Control/ApiRequestController.cs
1
using System;
2
using System.Net;
3
using System.Net.Http;
4
using System.Net.Http.Headers;
5
using System.Text;
6
using System.Text.Json;
7
using System.Threading;
8
using System.Threading.Tasks;
9
using TrelloDotNet.Model;
10

11
namespace TrelloDotNet.Control
12
{
13
    internal class ApiRequestController
14
    {
15
        private const string BaseUrl = "https://api.trello.com/1/";
16
        private const string NonApiBaseUrl = "https://trello.com/1/";
17
        private readonly HttpClient _httpClient;
18
        private readonly string _apiKey;
19
        private readonly string _token;
20
        private readonly TrelloClient _client;
21

22
        internal HttpClient HttpClient => _httpClient;
3✔
23

24
        internal ApiRequestController(HttpClient httpClient, string apiKey, string token, TrelloClient client)
1,297✔
25
        {
26
            _httpClient = httpClient;
1,297✔
27
            _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
1,297✔
28
            _apiKey = apiKey;
1,297✔
29
            _token = token;
1,297✔
30
            _client = client;
1,297✔
31
        }
1,297✔
32

33
        public string Token => _token;
36✔
34

35
        public string ApiKey => _apiKey;
1✔
36

37
        internal async Task<T> Get<T>(string suffix, CancellationToken cancellationToken, params QueryParameter[] parameters)
38
        {
39
            string json = await Get(suffix, cancellationToken, 0, parameters);
899✔
40
            T @object = JsonSerializer.Deserialize<T>(json);
899✔
41
            return @object;
899✔
42
        }
899✔
43

44
        internal async Task<string> Get(string suffix, CancellationToken cancellationToken, int retryCount, params QueryParameter[] parameters)
45
        {
46
            Uri uri = BuildUri(suffix, parameters);
924✔
47
            HttpResponseMessage response;
48
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri))
924✔
49
            {
50
                AddCredentialsToHeaderIfNeeded(request);
924✔
51
                response = await _httpClient.SendAsync(request, cancellationToken);
924✔
52
            }
924✔
53
            string responseContent = await response.Content.ReadAsStringAsync();
924✔
54

55
            if (response.StatusCode != HttpStatusCode.OK)
924✔
56
            {
57
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => Get(suffix, cancellationToken, retry, parameters), retryCount, cancellationToken);
10✔
58
            }
59

60
            return responseContent; //Content is assumed JSON
917✔
61
        }
920✔
62

63
        internal async Task<T> Post<T>(string suffix, CancellationToken cancellationToken, params QueryParameter[] parameters)
64
        {
65
            string json = await Post(suffix, cancellationToken, 0, parameters);
329✔
66
            T @object = JsonSerializer.Deserialize<T>(json);
329✔
67
            return @object;
329✔
68
        }
329✔
69

70
        internal async Task<T> PostWithAttachmentFileUpload<T>(string suffix, AttachmentFileUpload attachmentFile, CancellationToken cancellationToken, params QueryParameter[] parameters)
71
        {
72
            string json = await PostWithAttachmentFileUpload(suffix, attachmentFile, cancellationToken, 0, parameters);
2✔
73
            T @object = JsonSerializer.Deserialize<T>(json);
2✔
74
            return @object;
2✔
75
        }
2✔
76

77
        internal async Task<string> PostWithAttachmentFileUpload(string suffix, AttachmentFileUpload attachmentFile, CancellationToken cancellationToken, int retryCount, params QueryParameter[] parameters)
78
        {
79
            Uri uri = BuildUri(suffix, parameters);
2✔
80
            using (MultipartFormDataContent multipartFormContent = new MultipartFormDataContent())
2✔
81
            {
82
                multipartFormContent.Add(new StreamContent(attachmentFile.Stream), name: "file", fileName: attachmentFile.Filename);
2✔
83
                HttpResponseMessage response;
84
                using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri))
2✔
85
                {
86
                    request.Content = multipartFormContent;
2✔
87
                    AddCredentialsToHeaderIfNeeded(request);
2✔
88
                    response = await _httpClient.SendAsync(request, cancellationToken);
2✔
89
                }
2✔
90
                string responseContent = await response.Content.ReadAsStringAsync();
2✔
91

92
                if (response.StatusCode != HttpStatusCode.OK)
2!
93
                {
94
                    return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => PostWithAttachmentFileUpload(suffix, attachmentFile, cancellationToken, retry, parameters), retryCount, cancellationToken);
×
95
                }
96

97
                return responseContent; //Content is assumed JSON
2✔
98
            }
99
        }
2✔
100

101
        internal async Task<string> Post(string suffix, CancellationToken cancellationToken, int retryCount, params QueryParameter[] parameters)
102
        {
103
            Uri uri = BuildUri(suffix, parameters);
334✔
104
            HttpResponseMessage response;
105
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri))
334✔
106
            {
107
                AddCredentialsToHeaderIfNeeded(request);
334✔
108
                response = await _httpClient.SendAsync(request, cancellationToken);
334✔
109
            }
334✔
110
            string content = await response.Content.ReadAsStringAsync();
334✔
111

112
            if (response.StatusCode != HttpStatusCode.OK)
334✔
113
            {
114
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, content, retry => Post(suffix, cancellationToken, retry, parameters), retryCount, cancellationToken);
3✔
115
            }
116

117
            return content; //Content is assumed JSON
332✔
118
        }
333✔
119

120
        internal async Task<T> Put<T>(string suffix, CancellationToken cancellationToken, params QueryParameter[] parameters)
121
        {
122
            string json = await Put(suffix, cancellationToken, 0, parameters);
135✔
123
            T @object = JsonSerializer.Deserialize<T>(json);
135✔
124
            return @object;
135✔
125
        }
135✔
126

127
        internal async Task<string> Put(string suffix, CancellationToken cancellationToken, int retryCount, params QueryParameter[] parameters)
128
        {
129
            Uri uri = BuildUri(suffix, parameters);
143✔
130
            HttpResponseMessage response;
131
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, uri))
143✔
132
            {
133
                AddCredentialsToHeaderIfNeeded(request);
143✔
134
                response = await _httpClient.SendAsync(request, cancellationToken);
143✔
135
            }
143✔
136
            string responseContent = await response.Content.ReadAsStringAsync();
143✔
137

138
            if (response.StatusCode != HttpStatusCode.OK)
143✔
139
            {
140
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => Put(suffix, cancellationToken, retry, parameters), retryCount, cancellationToken);
5✔
141
            }
142

143
            return responseContent; //Content is assumed JSON
140✔
144
        }
142✔
145

146
        internal async Task<T> PutWithJsonPayload<T>(string suffix, CancellationToken cancellationToken, string payload, params QueryParameter[] parameters)
147
        {
148
            string json = await PutWithJsonPayload(suffix, cancellationToken, payload, 0, parameters);
18✔
149
            T @object = JsonSerializer.Deserialize<T>(json);
18✔
150
            return @object;
18✔
151
        }
18✔
152

153
        internal async Task<string> PutWithJsonPayload(string suffix, CancellationToken cancellationToken, string payload, int retryCount, params QueryParameter[] parameters)
154
        {
155
            Uri uri = BuildUri(suffix, parameters);
36✔
156
            HttpResponseMessage response;
157
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Put, uri))
36✔
158
            {
159
                request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
36✔
160
                AddCredentialsToHeaderIfNeeded(request);
36✔
161
                response = await _httpClient.SendAsync(request, cancellationToken);
36✔
162
            }
36✔
163
            string responseContent = await response.Content.ReadAsStringAsync();
36✔
164

165
            if (response.StatusCode != HttpStatusCode.OK)
36!
166
            {
167
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => PutWithJsonPayload(suffix, cancellationToken, payload, retry, parameters), retryCount, cancellationToken);
×
168
            }
169

170
            return responseContent; //Content is assumed JSON
36✔
171
        }
36✔
172

173
        internal async Task<T> PostWithJsonPayload<T>(string suffix, CancellationToken cancellationToken, string payload, params QueryParameter[] parameters)
174
        {
175
            string json = await PostWithJsonPayload(suffix, cancellationToken, payload, 0, parameters);
131✔
176
            T @object = JsonSerializer.Deserialize<T>(json);
131✔
177
            return @object;
131✔
178
        }
131✔
179

180
        internal async Task<string> PostWithJsonPayload(string suffix, CancellationToken cancellationToken, string payload, int retryCount, params QueryParameter[] parameters)
181
        {
182
            Uri uri = BuildUri(suffix, parameters);
132✔
183
            HttpResponseMessage response;
184
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri))
132✔
185
            {
186
                request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
132✔
187
                AddCredentialsToHeaderIfNeeded(request);
132✔
188
                response = await _httpClient.SendAsync(request, cancellationToken);
132✔
189
            }
132✔
190
            string responseContent = await response.Content.ReadAsStringAsync();
132✔
191

192
            if (response.StatusCode != HttpStatusCode.OK)
132✔
193
            {
194
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => PostWithJsonPayload(suffix, cancellationToken, payload, retry, parameters), retryCount, cancellationToken);
2✔
195
            }
196

197
            return responseContent; //Content is assumed JSON
131✔
198
        }
132✔
199

200
        internal async Task<string> PostWithJsonPayloadToNonApi(string suffix, CancellationToken cancellationToken, string payload, int retryCount, params QueryParameter[] parameters)
201
        {
202
            Uri uri = BuildNonApiUri(suffix, parameters);
×
203
            HttpResponseMessage response;
204
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri))
×
205
            {
206
                request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
×
207
                AddCredentialsToHeaderIfNeeded(request);
×
208
                response = await _httpClient.SendAsync(request, cancellationToken);
×
209
            }
×
210
            string responseContent = await response.Content.ReadAsStringAsync();
×
211

212
            if (response.StatusCode != HttpStatusCode.OK)
×
213
            {
214
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => PostWithJsonPayload(suffix, cancellationToken, payload, retry, parameters), retryCount, cancellationToken);
×
215
            }
216

217
            return responseContent; //Content is assumed JSON
×
218
        }
×
219

220
        private string FormatExceptionUrlAccordingToClientOptions(string fullUrl)
221
        {
222
            switch (_client.Options.ApiCallExceptionOption)
7!
223
            {
224
                case ApiCallExceptionOption.IncludeUrlAndCredentials:
225
                    return fullUrl;
1✔
226
                case ApiCallExceptionOption.IncludeUrlButMaskCredentials:
227
                    // ReSharper disable StringLiteralTypo
228
                    return fullUrl.Replace($"key={_apiKey}&token={_token}", "key=XXXXX&token=XXXXXXXXXX");
5✔
229
                case ApiCallExceptionOption.DoNotIncludeTheUrl:
230
                    return string.Empty.PadLeft(5, 'X');
1✔
231
                default:
232
                    throw new ArgumentOutOfRangeException();
×
233
            }
234
        }
235

236
        internal static StringBuilder GetParametersAsString(QueryParameter[] parameters)
237
        {
238
            StringBuilder parameterString = new StringBuilder();
1,674✔
239
            if (parameters == null || parameters.Length == 0)
1,674✔
240
            {
241
                return parameterString;
531✔
242
            }
243

244
            foreach (QueryParameter parameter in parameters)
18,532✔
245
            {
246
                parameterString.Append($"&{parameter.Name}={parameter.GetValueAsApiFormattedString()}");
8,123✔
247
            }
248

249
            return parameterString;
1,143✔
250
        }
251

252
        internal int GetQueryStringLength(params QueryParameter[] parameters)
253
        {
254
            return GetQueryStringCredentialPrefixLength() + GetParametersAsString(parameters).Length;
×
255
        }
256

257
        private Uri BuildUri(string suffix, params QueryParameter[] parameters)
258
        {
259
            string queryString = GetQueryStringCredentials();
1,663✔
260
            string additionalParameters = GetParametersAsString(parameters).ToString();
1,663✔
261
            if (string.IsNullOrWhiteSpace(queryString))
1,663✔
262
            {
263
                additionalParameters = additionalParameters.TrimStart('&');
1✔
264
            }
265

266
            if (string.IsNullOrWhiteSpace(queryString) && string.IsNullOrWhiteSpace(additionalParameters))
1,663✔
267
            {
268
                return new Uri($"{BaseUrl}{suffix}");
1✔
269
            }
270

271
            string separator = suffix.Contains("?") ? "&" : "?";
1,662✔
272
            return new Uri($"{BaseUrl}{suffix}{separator}{queryString}{additionalParameters}");
1,662✔
273
        }
274
        
275
        private Uri BuildNonApiUri(string suffix, params QueryParameter[] parameters)
276
        {
277
            string queryString = GetQueryStringCredentials();
×
278
            string additionalParameters = GetParametersAsString(parameters).ToString();
×
279
            if (string.IsNullOrWhiteSpace(queryString))
×
280
            {
281
                additionalParameters = additionalParameters.TrimStart('&');
×
282
            }
283

284
            if (string.IsNullOrWhiteSpace(queryString) && string.IsNullOrWhiteSpace(additionalParameters))
×
285
            {
286
                return new Uri($"{NonApiBaseUrl}{suffix}");
×
287
            }
288

289
            string separator = suffix.Contains("?") ? "&" : "?";
×
290
            return new Uri($"{NonApiBaseUrl}{suffix}{separator}{queryString}{additionalParameters}");
×
291
        }
292

293

294
        internal async Task<string> Delete(string suffix, CancellationToken cancellationToken, int retryCount)
295
        {
296
            Uri uri = BuildUri(suffix);
92✔
297
            HttpResponseMessage response;
298
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, uri))
92✔
299
            {
300
                AddCredentialsToHeaderIfNeeded(request);
92✔
301
                response = await _httpClient.SendAsync(request, cancellationToken);
92✔
302
            }
92✔
303
            string responseContent = await response.Content.ReadAsStringAsync();
92✔
304

305
            if (response.StatusCode != HttpStatusCode.OK)
92✔
306
            {
307
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => Delete(suffix, cancellationToken, retry), retryCount, cancellationToken);
3✔
308
            }
309

310
            return null;
90✔
311
        }
91✔
312

313
        internal async Task<string> DeleteToNonApi(string suffix, CancellationToken cancellationToken, int retryCount)
314
        {
315
            Uri uri = BuildNonApiUri(suffix);
×
316
            HttpResponseMessage response;
317
            using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Delete, uri))
×
318
            {
319
                AddCredentialsToHeaderIfNeeded(request);
×
320
                response = await _httpClient.SendAsync(request, cancellationToken);
×
321
            }
×
322
            string responseContent = await response.Content.ReadAsStringAsync();
×
323

324
            if (response.StatusCode != HttpStatusCode.OK)
×
325
            {
326
                return await PreformRetryIfNeededOrThrow(uri, response.StatusCode, responseContent, retry => Delete(suffix, cancellationToken, retry), retryCount, cancellationToken);
×
327
            }
328

329
            return null;
×
330
        }
×
331

332
        private async Task<string> PreformRetryIfNeededOrThrow(Uri uri, HttpStatusCode statusCode, string responseContent, Func<int, Task<string>> toRetry, int retryCount, CancellationToken cancellationToken)
333
        {
334
            int statusCodeAsInteger = (int)statusCode;
15✔
335
            bool isTooManyRequestsStatusCode = statusCodeAsInteger == 429;
15✔
336
            if ((responseContent.Contains("API_TOKEN_LIMIT_EXCEEDED") || isTooManyRequestsStatusCode) && retryCount <= _client.Options.MaxRetryCountForTokenLimitExceeded)
15✔
337
            {
338
                await Task.Delay(TimeSpan.FromSeconds(_client.Options.DelayInSecondsToWaitInTokenLimitExceededRetry), cancellationToken);
8✔
339
                retryCount++;
8✔
340
                return await toRetry(retryCount);
8✔
341
            }
342

343
            throw new TrelloApiException($"{responseContent} [{statusCodeAsInteger}: {statusCode}]", FormatExceptionUrlAccordingToClientOptions(uri.AbsoluteUri), statusCode); //Content is assumed Error Message       
7✔
344
        }
8✔
345

346
        private int GetQueryStringCredentialPrefixLength()
347
        {
348
            if (_client.Options.SendCredentialsMode == SendCredentialsMode.Header)
×
349
            {
350
                return 0;
×
351
            }
352

353
            return $"?{GetQueryStringCredentials()}".Length;
×
354
        }
355

356
        private string GetQueryStringCredentials()
357
        {
358
            if (_client.Options.SendCredentialsMode == SendCredentialsMode.Header)
1,663✔
359
            {
360
                return string.Empty;
1✔
361
            }
362

363
            return $"key={_apiKey}&token={_token}";
1,662✔
364
        }
365

366
        private void AddCredentialsToHeaderIfNeeded(HttpRequestMessage request)
367
        {
368
            if (_client.Options.SendCredentialsMode != SendCredentialsMode.Header)
1,663✔
369
            {
370
                return;
1,662✔
371
            }
372

373
            request.Headers.Authorization = AuthenticationHeaderValue.Parse($"OAuth oauth_consumer_key=\"{_apiKey}\", oauth_token=\"{_token}\"");
1✔
374
        }
1✔
375
    }
376
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc