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

moconnell / yolo / 29190218565

12 Jul 2026 11:04AM UTC coverage: 87.362% (+0.3%) from 87.024%
29190218565

push

github

web-flow
Trade ingestion (#134)

* feat(trade-ingestion): initial commit

* fix: review snag list

* fix: test coverage

* refactor: rename timer functions in line with existing convention

* refactor(trade-ingestion): move to durable functions

* refactor(trade-ingestion): chunk ingestion activities

* feat(TradeIngestionDurableWorkflow): add more logging

* fix(appsettings.json): update trade ingestion config

* fix(TradeIngestionDurableWorkflow): add retry options

* fix(OrderManager): orphaned replacement orders bug

459 of 497 branches covered (92.35%)

Branch coverage included in aggregate %.

749 of 844 new or added lines in 21 files covered. (88.74%)

4276 of 4923 relevant lines covered (86.86%)

20.95 hits per line

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

79.36
/src/YoloFunk/Functions/StorageQueryFunctions.cs
1
using System.Net;
2
using Azure;
3
using Azure.Data.Tables;
4
using Azure.Storage.Blobs;
5
using Microsoft.Azure.Functions.Worker;
6
using Microsoft.Azure.Functions.Worker.Http;
7
using Microsoft.Extensions.DependencyInjection;
8
using Microsoft.Extensions.Logging;
9
using YoloFunk.Dto;
10
using YoloFunk.Infrastructure;
11

12
namespace YoloFunk.Functions;
13

14
public sealed class StorageQueryFunctions(
9✔
15
    IServiceProvider serviceProvider,
9✔
16
    ILogger<StorageQueryFunctions> logger)
9✔
17
{
18
    private const string RebalanceEventsTableName = "rebalanceevents";
19
    private const string HttpRequestsTableName = "httprequestsindex";
20
    private const string UserTradesTableName = "usertrades";
21
    private const string HttpRequestsContainerName = "http-requests";
22

23
    [Function(nameof(GetRebalanceEvents))]
24
    public async Task<HttpResponseData> GetRebalanceEvents(
25
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/rebalance-events")]
26
        HttpRequestData req,
27
        CancellationToken cancellationToken)
28
    {
4✔
29
        if (!TryGetTableServiceClient(req, out var tableServiceClient))
4✔
30
            return await ServiceUnavailableAsync(req, cancellationToken);
×
31

32
        var query = HttpQueryParameters.Parse(req.Url);
4✔
33
        var page = query.GetInt32("page", 1, 1, 10_000);
4✔
34
        var pageSize = query.GetInt32("pageSize", 100, 1, 500);
4✔
35
        var orderBy = NormalizeTableOrderBy(query.GetString("orderBy"));
4✔
36
        var direction = NormalizeTableDirection(query.GetString("direction"));
4✔
37
        var continuationToken = query.GetString("continuationToken");
4✔
38
        if (orderBy is null || direction is null)
4✔
39
            return await InvalidPagedTableSortAsync(req, cancellationToken);
×
40

41
        try
42
        {
4✔
43
            var pageResult = await QueryRebalanceEventsAsync(
4✔
44
                tableServiceClient,
4✔
45
                query,
4✔
46
                pageSize,
4✔
47
                continuationToken,
4✔
48
                cancellationToken);
4✔
49

50
            return await WritePagedResponseAsync(
4✔
51
                req,
4✔
52
                pageResult.Items,
4✔
53
                page,
4✔
54
                pageSize,
4✔
55
                orderBy,
4✔
56
                direction,
4✔
57
                cancellationToken,
4✔
58
                pageResult.NextContinuationToken,
4✔
59
                skipItems: false);
4✔
60
        }
61
        catch (RequestFailedException ex) when (ex.Status == 404)
×
62
        {
×
63
            return await WritePagedResponseAsync(
×
64
                req,
×
65
                Array.Empty<RebalanceEventQueryItem>(),
×
66
                page,
×
67
                pageSize,
×
68
                orderBy,
×
69
                direction,
×
70
                cancellationToken);
×
71
        }
72
        catch (Exception ex)
×
73
        {
×
74
            logger.LogError(ex, "Failed to query rebalance events");
×
75
            return await ErrorAsync(req, "Failed to query rebalance events", cancellationToken);
×
76
        }
77
    }
4✔
78

79
    [Function(nameof(GetHttpRequestCaptures))]
80
    public async Task<HttpResponseData> GetHttpRequestCaptures(
81
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/http-requests")]
82
        HttpRequestData req,
83
        CancellationToken cancellationToken)
84
    {
2✔
85
        if (!TryGetTableServiceClient(req, out var tableServiceClient))
2✔
86
            return await ServiceUnavailableAsync(req, cancellationToken);
×
87

88
        var query = HttpQueryParameters.Parse(req.Url);
2✔
89
        var page = query.GetInt32("page", 1, 1, 10_000);
2✔
90
        var pageSize = query.GetInt32("pageSize", 100, 1, 500);
2✔
91
        var orderBy = NormalizeTableOrderBy(query.GetString("orderBy"));
2✔
92
        var direction = NormalizeTableDirection(query.GetString("direction"));
2✔
93
        var continuationToken = query.GetString("continuationToken");
2✔
94
        if (orderBy is null || direction is null)
2✔
95
            return await InvalidPagedTableSortAsync(req, cancellationToken);
×
96

97
        try
98
        {
2✔
99
            var pageResult = await QueryHttpRequestCapturesAsync(
2✔
100
                tableServiceClient,
2✔
101
                query,
2✔
102
                pageSize,
2✔
103
                continuationToken,
2✔
104
                cancellationToken);
2✔
105

106
            return await WritePagedResponseAsync(
2✔
107
                req,
2✔
108
                pageResult.Items,
2✔
109
                page,
2✔
110
                pageSize,
2✔
111
                orderBy,
2✔
112
                direction,
2✔
113
                cancellationToken,
2✔
114
                pageResult.NextContinuationToken,
2✔
115
                skipItems: false);
2✔
116
        }
117
        catch (RequestFailedException ex) when (ex.Status == 404)
×
118
        {
×
119
            return await WritePagedResponseAsync(
×
120
                req,
×
121
                Array.Empty<HttpRequestCaptureQueryItem>(),
×
122
                page,
×
123
                pageSize,
×
124
                orderBy,
×
125
                direction,
×
126
                cancellationToken);
×
127
        }
128
        catch (Exception ex)
×
129
        {
×
130
            logger.LogError(ex, "Failed to query HTTP request captures");
×
131
            return await ErrorAsync(req, "Failed to query HTTP request captures", cancellationToken);
×
132
        }
133
    }
2✔
134

135
    [Function(nameof(GetHttpRequestPayload))]
136
    public async Task<HttpResponseData> GetHttpRequestPayload(
137
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/http-requests/payload")]
138
        HttpRequestData req,
139
        CancellationToken cancellationToken)
140
    {
1✔
141
        if (!TryGetBlobServiceClient(req, out var blobServiceClient))
1✔
142
            return await ServiceUnavailableAsync(req, cancellationToken);
×
143

144
        var blobName = HttpQueryParameters.Parse(req.Url).GetString("blobName");
1✔
145
        if (string.IsNullOrWhiteSpace(blobName))
1✔
146
        {
1✔
147
            var badRequest = req.CreateResponse(HttpStatusCode.BadRequest);
1✔
148
            await badRequest.WriteAsJsonAsync(
1✔
149
                new { Error = "Missing required query parameter: blobName" },
1✔
150
                cancellationToken);
1✔
151
            return badRequest;
1✔
152
        }
153

154
        try
155
        {
×
156
            var blobClient = blobServiceClient
×
157
                .GetBlobContainerClient(HttpRequestsContainerName)
×
158
                .GetBlobClient(blobName);
×
159
            var download = await blobClient.DownloadContentAsync(cancellationToken);
×
160

161
            var response = req.CreateResponse(HttpStatusCode.OK);
×
162
            response.Headers.Add("Content-Type", "application/json");
×
163
            await response.WriteStringAsync(download.Value.Content.ToString(), cancellationToken);
×
164
            return response;
×
165
        }
166
        catch (RequestFailedException ex) when (ex.Status == 404)
×
167
        {
×
168
            var notFound = req.CreateResponse(HttpStatusCode.NotFound);
×
169
            await notFound.WriteAsJsonAsync(
×
170
                new { Error = "HTTP request payload not found", BlobName = blobName },
×
171
                cancellationToken);
×
172
            return notFound;
×
173
        }
174
        catch (Exception ex)
×
175
        {
×
176
            logger.LogError(ex, "Failed to get HTTP request payload {BlobName}", blobName);
×
177
            return await ErrorAsync(req, "Failed to get HTTP request payload", cancellationToken);
×
178
        }
179
    }
1✔
180

181
    [Function(nameof(GetUserTrades))]
182
    public async Task<HttpResponseData> GetUserTrades(
183
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "storage/user-trades")]
184
        HttpRequestData req,
185
        CancellationToken cancellationToken)
186
    {
2✔
187
        if (!TryGetTableServiceClient(req, out var tableServiceClient))
2✔
NEW
188
            return await ServiceUnavailableAsync(req, cancellationToken);
×
189

190
        var query = HttpQueryParameters.Parse(req.Url);
2✔
191
        var page = query.GetInt32("page", 1, 1, 10_000);
2✔
192
        var pageSize = query.GetInt32("pageSize", 100, 1, 500);
2✔
193
        var orderBy = NormalizeTableOrderBy(query.GetString("orderBy"));
2✔
194
        var direction = NormalizeTableDirection(query.GetString("direction"));
2✔
195
        var continuationToken = query.GetString("continuationToken");
2✔
196
        if (orderBy is null || direction is null)
2✔
NEW
197
            return await InvalidPagedTableSortAsync(req, cancellationToken);
×
198

199
        try
200
        {
2✔
201
            var pageResult = await QueryUserTradesAsync(
2✔
202
                tableServiceClient,
2✔
203
                query,
2✔
204
                pageSize,
2✔
205
                continuationToken,
2✔
206
                cancellationToken);
2✔
207

208
            return await WritePagedResponseAsync(
2✔
209
                req,
2✔
210
                pageResult.Items,
2✔
211
                page,
2✔
212
                pageSize,
2✔
213
                orderBy,
2✔
214
                direction,
2✔
215
                cancellationToken,
2✔
216
                pageResult.NextContinuationToken,
2✔
217
                skipItems: false);
2✔
218
        }
NEW
219
        catch (RequestFailedException ex) when (ex.Status == 404)
×
NEW
220
        {
×
NEW
221
            return await WritePagedResponseAsync(
×
NEW
222
                req,
×
NEW
223
                Array.Empty<UserTradeQueryItem>(),
×
NEW
224
                page,
×
NEW
225
                pageSize,
×
NEW
226
                orderBy,
×
NEW
227
                direction,
×
NEW
228
                cancellationToken);
×
229
        }
NEW
230
        catch (Exception ex)
×
NEW
231
        {
×
NEW
232
            logger.LogError(ex, "Failed to query user trades");
×
NEW
233
            return await ErrorAsync(req, "Failed to query user trades", cancellationToken);
×
234
        }
235
    }
2✔
236

237
    private bool TryGetTableServiceClient(
238
        HttpRequestData req,
239
        out TableServiceClient tableServiceClient)
240
    {
8✔
241
        tableServiceClient = serviceProvider.GetService<TableServiceClient>() ??
8✔
242
                             req.FunctionContext.InstanceServices.GetService<TableServiceClient>()!;
8✔
243

244
        return tableServiceClient is not null;
8✔
245
    }
8✔
246

247
    private bool TryGetBlobServiceClient(
248
        HttpRequestData req,
249
        out BlobServiceClient blobServiceClient)
250
    {
1✔
251
        blobServiceClient = serviceProvider.GetService<BlobServiceClient>() ??
1✔
252
                            req.FunctionContext.InstanceServices.GetService<BlobServiceClient>()!;
1✔
253

254
        return blobServiceClient is not null;
1✔
255
    }
1✔
256

257
    private static async Task<QueryPageResult<HttpRequestCaptureQueryItem>> QueryHttpRequestCapturesAsync(
258
        TableServiceClient tableServiceClient,
259
        HttpQueryParameters query,
260
        int pageSize,
261
        string? continuationToken,
262
        CancellationToken cancellationToken)
263
    {
2✔
264
        var host = query.GetString("host");
2✔
265
        var endpoint = query.GetString("endpoint");
2✔
266
        var method = query.GetString("method");
2✔
267
        var statusCode = query.GetString("statusCode");
2✔
268
        var contentHash = query.GetString("contentHash");
2✔
269
        var from = query.GetDateTimeOffset("from");
2✔
270
        var to = query.GetDateTimeOffset("to");
2✔
271

272
        var tableClient = tableServiceClient.GetTableClient(HttpRequestsTableName);
2✔
273
        return await QueryFilteredEntitiesPageAsync(
2✔
274
            tableClient,
2✔
275
            BuildPartitionFilter(host),
2✔
276
            pageSize,
2✔
277
            continuationToken,
2✔
278
            ToHttpRequestCaptureQueryItem,
2✔
279
            item => Matches(item.Host, host) &&
4✔
280
                    Contains(item.Endpoint, endpoint) &&
4✔
281
                    Matches(item.Method, method) &&
4✔
282
                    (!int.TryParse(statusCode, out var expectedStatusCode) || item.StatusCode == expectedStatusCode) &&
4✔
283
                    Matches(item.ContentHash, contentHash) &&
4✔
284
                    (!from.HasValue || item.RequestTimeUtc >= from.Value) &&
4✔
285
                    (!to.HasValue || item.RequestTimeUtc <= to.Value),
4✔
286
            cancellationToken);
2✔
287
    }
2✔
288

289
    private static async Task<QueryPageResult<RebalanceEventQueryItem>> QueryRebalanceEventsAsync(
290
        TableServiceClient tableServiceClient,
291
        HttpQueryParameters query,
292
        int pageSize,
293
        string? continuationToken,
294
        CancellationToken cancellationToken)
295
    {
4✔
296
        var strategy = query.GetString("strategy");
4✔
297
        var runId = query.GetString("runId");
4✔
298
        var eventType = query.GetString("eventType");
4✔
299
        var level = query.GetString("level");
4✔
300
        var coin = query.GetString("coin");
4✔
301
        var clientOrderId = query.GetString("clientOrderId");
4✔
302
        var from = query.GetDateTimeOffset("from");
4✔
303
        var to = query.GetDateTimeOffset("to");
4✔
304

305
        var tableClient = tableServiceClient.GetTableClient(RebalanceEventsTableName);
4✔
306
        var partitionFilter = !string.IsNullOrWhiteSpace(strategy) && !string.IsNullOrWhiteSpace(runId)
4✔
307
            ? BuildPartitionFilter($"{strategy}|{runId}")
4✔
308
            : null;
4✔
309
        return await QueryFilteredEntitiesPageAsync(
4✔
310
            tableClient,
4✔
311
            partitionFilter,
4✔
312
            pageSize,
4✔
313
            continuationToken,
4✔
314
            ToRebalanceEventQueryItem,
4✔
315
            item => Matches(item.StrategyName, strategy) &&
13✔
316
                    Matches(item.RunId, runId) &&
13✔
317
                    Matches(item.EventType, eventType) &&
13✔
318
                    Matches(item.Level, level) &&
13✔
319
                    Matches(item.Coin, coin) &&
13✔
320
                    Matches(item.ClientOrderId, clientOrderId) &&
13✔
321
                    (!from.HasValue || item.TimestampUtc >= from.Value) &&
13✔
322
                    (!to.HasValue || item.TimestampUtc <= to.Value),
13✔
323
            cancellationToken);
4✔
324
    }
4✔
325

326
    private static async Task<QueryPageResult<UserTradeQueryItem>> QueryUserTradesAsync(
327
        TableServiceClient tableServiceClient,
328
        HttpQueryParameters query,
329
        int pageSize,
330
        string? continuationToken,
331
        CancellationToken cancellationToken)
332
    {
2✔
333
        var strategy = query.GetString("strategy");
2✔
334
        var exchange = query.GetString("exchange");
2✔
335
        var network = query.GetString("network");
2✔
336
        var address = query.GetString("address");
2✔
337
        var vaultAddress = query.GetString("vaultAddress");
2✔
338
        var symbol = query.GetString("symbol");
2✔
339
        var exchangeSymbol = query.GetString("exchangeSymbol");
2✔
340
        var clientOrderId = query.GetString("clientOrderId");
2✔
341
        var orderId = query.GetString("orderId");
2✔
342
        var tradeId = query.GetString("tradeId");
2✔
343
        var hash = query.GetString("hash");
2✔
344
        var from = query.GetDateTimeOffset("from");
2✔
345
        var to = query.GetDateTimeOffset("to");
2✔
346

347
        var partitionAddress = !string.IsNullOrWhiteSpace(vaultAddress) ? vaultAddress : address;
2✔
348
        var partitionFilter = !string.IsNullOrWhiteSpace(strategy) &&
2✔
349
                              !string.IsNullOrWhiteSpace(exchange) &&
2✔
350
                              !string.IsNullOrWhiteSpace(network) &&
2✔
351
                              !string.IsNullOrWhiteSpace(partitionAddress)
2✔
352
            ? BuildPartitionFilter($"{strategy}|{exchange}|{network}|{partitionAddress}")
2✔
353
            : null;
2✔
354

355
        var tableClient = tableServiceClient.GetTableClient(UserTradesTableName);
2✔
356
        return await QueryFilteredEntitiesPageAsync(
2✔
357
            tableClient,
2✔
358
            partitionFilter,
2✔
359
            pageSize,
2✔
360
            continuationToken,
2✔
361
            ToUserTradeQueryItem,
2✔
362
            item => Matches(item.StrategyName, strategy) &&
5✔
363
                    Matches(item.Exchange, exchange) &&
5✔
364
                    Matches(item.Network, network) &&
5✔
365
                    Matches(item.Address, address) &&
5✔
366
                    Matches(item.VaultAddress, vaultAddress) &&
5✔
367
                    Matches(item.Symbol, symbol) &&
5✔
368
                    Matches(item.ExchangeSymbol, exchangeSymbol) &&
5✔
369
                    Matches(item.ClientOrderId, clientOrderId) &&
5✔
370
                    Matches(item.Hash, hash) &&
5✔
371
                    (!long.TryParse(orderId, out var expectedOrderId) || item.OrderId == expectedOrderId) &&
5✔
372
                    (!long.TryParse(tradeId, out var expectedTradeId) || item.TradeId == expectedTradeId) &&
5✔
373
                    (!from.HasValue || item.TimestampUtc >= from.Value) &&
5✔
374
                    (!to.HasValue || item.TimestampUtc <= to.Value),
5✔
375
            cancellationToken);
2✔
376
    }
2✔
377

378
    private static async Task<QueryPageResult<T>> QueryFilteredEntitiesPageAsync<T>(
379
        TableClient tableClient,
380
        string? filter,
381
        int pageSize,
382
        string? continuationToken,
383
        Func<TableEntity, T> map,
384
        Func<T, bool> predicate,
385
        CancellationToken cancellationToken)
386
    {
8✔
387
        var items = new List<T>(pageSize);
8✔
388

389
        await foreach (var page in tableClient
51✔
390
                           .QueryAsync<TableEntity>(
8✔
391
                               filter,
8✔
392
                               maxPerPage: 1,
8✔
393
                               cancellationToken: cancellationToken)
8✔
394
                           .AsPages(continuationToken, 1))
8✔
395
        {
15✔
396
            foreach (var entity in page.Values)
86✔
397
            {
22✔
398
                var item = map(entity);
22✔
399
                if (!predicate(item))
22✔
400
                {
10✔
401
                    continue;
10✔
402
                }
403

404
                items.Add(item);
12✔
405
                if (items.Count == pageSize)
12✔
406
                {
3✔
407
                    return new QueryPageResult<T>(items, page.ContinuationToken);
3✔
408
                }
409
            }
9✔
410
        }
12✔
411

412
        return new QueryPageResult<T>(items, null);
5✔
413
    }
8✔
414

415
    private static string? BuildPartitionFilter(string? partitionKey)
416
    {
4✔
417
        return string.IsNullOrWhiteSpace(partitionKey)
4✔
418
            ? null
4✔
419
            : $"PartitionKey eq '{SanitizeTableKey(partitionKey).Replace("'", "''")}'";
4✔
420
    }
4✔
421

422
    private static async Task<HttpResponseData> WritePagedResponseAsync<T>(
423
        HttpRequestData req,
424
        IReadOnlyList<T> items,
425
        int page,
426
        int pageSize,
427
        string? orderBy,
428
        string direction,
429
        CancellationToken cancellationToken,
430
        string? nextContinuationToken = null,
431
        bool skipItems = true)
432
    {
8✔
433
        var response = req.CreateResponse(HttpStatusCode.OK);
8✔
434
        var pageItems = skipItems
8✔
435
            ? items
8✔
436
                .Skip((page - 1) * pageSize)
8✔
437
                .Take(pageSize)
8✔
438
                .ToArray()
8✔
439
            : [.. items.Take(pageSize)];
8✔
440

441
        await response.WriteAsJsonAsync(
8✔
442
            new PagedQueryResponse<T>(pageItems, page, pageSize, items.Count, orderBy, direction, nextContinuationToken),
8✔
443
            cancellationToken);
8✔
444
        return response;
8✔
445
    }
8✔
446

447
    private static HttpRequestCaptureQueryItem ToHttpRequestCaptureQueryItem(TableEntity entity)
448
    {
4✔
449
        return new HttpRequestCaptureQueryItem(
4✔
450
            GetString(entity, "Host"),
4✔
451
            GetString(entity, "Endpoint"),
4✔
452
            GetString(entity, "Url"),
4✔
453
            GetString(entity, "Method"),
4✔
454
            GetInt32(entity, "StatusCode"),
4✔
455
            GetString(entity, "BlobContainer"),
4✔
456
            GetString(entity, "BlobName"),
4✔
457
            GetString(entity, "ContentHash"),
4✔
458
            GetDateTimeOffset(entity, "RequestTimeUtc"),
4✔
459
            GetString(entity, "QueryParametersJson"));
4✔
460
    }
4✔
461

462
    private static RebalanceEventQueryItem ToRebalanceEventQueryItem(TableEntity entity)
463
    {
13✔
464
        return new RebalanceEventQueryItem(
13✔
465
            GetString(entity, "RunId"),
13✔
466
            GetString(entity, "StrategyName"),
13✔
467
            GetDateTimeOffset(entity, "TimestampUtc"),
13✔
468
            GetInt32(entity, "Sequence"),
13✔
469
            GetString(entity, "EventType"),
13✔
470
            GetString(entity, "Level"),
13✔
471
            GetString(entity, "Summary"),
13✔
472
            GetNullableString(entity, "WalletAddress"),
13✔
473
            GetNullableString(entity, "VaultAddress"),
13✔
474
            GetNullableString(entity, "Coin"),
13✔
475
            GetNullableString(entity, "ClientOrderId"),
13✔
476
            GetNullableString(entity, "OrderId"),
13✔
477
            GetString(entity, "PayloadJson"));
13✔
478
    }
13✔
479

480
    private static UserTradeQueryItem ToUserTradeQueryItem(TableEntity entity)
481
    {
5✔
482
        return new UserTradeQueryItem(
5✔
483
            entity.PartitionKey,
5✔
484
            entity.RowKey,
5✔
485
            GetString(entity, "StrategyName"),
5✔
486
            GetString(entity, "Exchange"),
5✔
487
            GetString(entity, "Network"),
5✔
488
            GetString(entity, "Address"),
5✔
489
            GetNullableString(entity, "VaultAddress"),
5✔
490
            GetDateTimeOffset(entity, "TimestampUtc"),
5✔
491
            GetString(entity, "ExchangeSymbol"),
5✔
492
            GetNullableString(entity, "Symbol"),
5✔
493
            GetNullableString(entity, "SymbolType"),
5✔
494
            GetNullableString(entity, "Direction"),
5✔
495
            GetNullableString(entity, "Hash"),
5✔
496
            GetNullableInt64(entity, "OrderId"),
5✔
497
            GetString(entity, "Price"),
5✔
498
            GetNullableString(entity, "OrderSide"),
5✔
499
            GetNullableString(entity, "StartPosition"),
5✔
500
            GetString(entity, "Quantity"),
5✔
501
            GetString(entity, "Fee"),
5✔
502
            GetNullableString(entity, "FeeToken"),
5✔
503
            GetNullableString(entity, "BuilderFee"),
5✔
504
            GetNullableString(entity, "ClosedPnl"),
5✔
505
            GetNullableBool(entity, "Crossed") ?? false,
5✔
506
            GetNullableInt64(entity, "TradeId"),
5✔
507
            GetNullableInt64(entity, "TwapId"),
5✔
508
            GetNullableString(entity, "ClientOrderId"),
5✔
509
            GetNullableString(entity, "LiquidationJson"),
5✔
510
            GetString(entity, "RawJson"));
5✔
511
    }
5✔
512

513
    private static bool Matches(string? actual, string? expected) =>
514
        string.IsNullOrWhiteSpace(expected) ||
113✔
515
        string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase);
113✔
516

517
    private static bool Contains(string actual, string? expected) =>
518
        string.IsNullOrWhiteSpace(expected) ||
4✔
519
        actual.Contains(expected, StringComparison.OrdinalIgnoreCase);
4✔
520

521
    private static string? NormalizeTableOrderBy(string? orderBy)
522
    {
8✔
523
        if (string.IsNullOrWhiteSpace(orderBy))
8✔
524
        {
8✔
525
            return "table";
8✔
526
        }
527

528
        return orderBy.Equals("table", StringComparison.OrdinalIgnoreCase) ||
×
529
               orderBy.Equals("partitionKey", StringComparison.OrdinalIgnoreCase) ||
×
530
               orderBy.Equals("rowKey", StringComparison.OrdinalIgnoreCase)
×
531
            ? "table"
×
532
            : null;
×
533
    }
8✔
534

535
    private static string? NormalizeTableDirection(string? direction)
536
    {
8✔
537
        if (string.IsNullOrWhiteSpace(direction) ||
8✔
538
            direction.Equals("asc", StringComparison.OrdinalIgnoreCase))
8✔
539
        {
8✔
540
            return "asc";
8✔
541
        }
542

543
        return null;
×
544
    }
8✔
545

546
    private static string SanitizeTableKey(string value)
547
    {
3✔
548
        return value
3✔
549
            .Replace("/", "|")
3✔
550
            .Replace("\\", "|")
3✔
551
            .Replace("#", "_")
3✔
552
            .Replace("?", "_");
3✔
553
    }
3✔
554

555
    private static string GetString(TableEntity entity, string property) =>
556
        entity.TryGetValue(property, out var value) ? value?.ToString() ?? string.Empty : string.Empty;
155✔
557

558
    private static string? GetNullableString(TableEntity entity, string property) =>
559
        entity.TryGetValue(property, out var value) ? value?.ToString() : null;
125✔
560

561
    private static int GetInt32(TableEntity entity, string property) =>
562
        entity.TryGetValue(property, out var value) && value is int parsed ? parsed : 0;
17✔
563

564
    private static long? GetNullableInt64(TableEntity entity, string property)
565
    {
15✔
566
        if (!entity.TryGetValue(property, out var value))
15✔
567
            return null;
5✔
568

569
        return value switch
10✔
570
        {
10✔
571
            long parsed => parsed,
10✔
NEW
572
            int parsed => parsed,
×
NEW
573
            _ => null
×
574
        };
10✔
575
    }
15✔
576

577
    private static bool? GetNullableBool(TableEntity entity, string property) =>
578
        entity.TryGetValue(property, out var value) && value is bool parsed ? parsed : null;
5✔
579

580
    private static DateTimeOffset GetDateTimeOffset(TableEntity entity, string property) =>
581
        GetNullableDateTimeOffset(entity, property) ?? DateTimeOffset.MinValue;
22✔
582

583
    private static DateTimeOffset? GetNullableDateTimeOffset(TableEntity entity, string property) =>
584
        entity.TryGetValue(property, out var value) && value is DateTimeOffset parsed ? parsed : null;
22✔
585

586
    private static async Task<HttpResponseData> ServiceUnavailableAsync(
587
        HttpRequestData req,
588
        CancellationToken cancellationToken)
589
    {
×
590
        var response = req.CreateResponse(HttpStatusCode.ServiceUnavailable);
×
591
        await response.WriteAsJsonAsync(
×
592
            new { Error = "Azure storage is not configured. Set AzureWebJobsStorage to enable storage query endpoints." },
×
593
            cancellationToken);
×
594
        return response;
×
595
    }
×
596

597
    private static async Task<HttpResponseData> ErrorAsync(
598
        HttpRequestData req,
599
        string message,
600
        CancellationToken cancellationToken)
601
    {
×
602
        var response = req.CreateResponse(HttpStatusCode.InternalServerError);
×
603
        await response.WriteAsJsonAsync(new { Error = message }, cancellationToken);
×
604
        return response;
×
605
    }
×
606

607
    private static async Task<HttpResponseData> InvalidPagedTableSortAsync(
608
        HttpRequestData req,
609
        CancellationToken cancellationToken)
610
    {
×
611
        var response = req.CreateResponse(HttpStatusCode.BadRequest);
×
612
        await response.WriteAsJsonAsync(
×
613
            new { Error = "Continuation-token paging supports only table order. Use orderBy=table and direction=asc." },
×
614
            cancellationToken);
×
615
        return response;
×
616
    }
×
617

618
    private sealed record QueryPageResult<T>(IReadOnlyList<T> Items, string? NextContinuationToken);
24✔
619
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc