mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-12-29 10:10:29 +00:00
Compare commits
571 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa1ebdc4ed | ||
|
|
38058c4a70 | ||
|
|
a7eb483479 | ||
|
|
c76931a3b4 | ||
|
|
b90b7e9e0c | ||
|
|
beda53d36d | ||
|
|
0668f669c1 | ||
|
|
64250e13db | ||
|
|
451d38d5e7 | ||
|
|
e11e437bbb | ||
|
|
b8a1ad798d | ||
|
|
4a851c44f2 | ||
|
|
d079796020 | ||
|
|
f125bc88b0 | ||
|
|
f3d535f286 | ||
|
|
6e8c6feec2 | ||
|
|
84b0444caf | ||
|
|
4be986ebe7 | ||
|
|
21872f818a | ||
|
|
4e7c45d758 | ||
|
|
0e55e5f065 | ||
|
|
b1c5cf318a | ||
|
|
9d94a24862 | ||
|
|
6cf9684f55 | ||
|
|
8043a48c49 | ||
|
|
7d657dd533 | ||
|
|
1bfdec1484 | ||
|
|
8d5b6a53f3 | ||
|
|
ad97102e7c | ||
|
|
d8dc121386 | ||
|
|
10c3868c00 | ||
|
|
411ba00a82 | ||
|
|
c1b2b62dbc | ||
|
|
3960cab7a7 | ||
|
|
34e9447e55 | ||
|
|
9d3295acc7 | ||
|
|
995cd3d84c | ||
|
|
919cdf0075 | ||
|
|
d181c9cfc1 | ||
|
|
5943142c44 | ||
|
|
dbc430e838 | ||
|
|
7413d03d31 | ||
|
|
dd60067684 | ||
|
|
04e4ddf525 | ||
|
|
99bf6d7c75 | ||
|
|
99a203933c | ||
|
|
b43d2a2040 | ||
|
|
ba9c406def | ||
|
|
f5f4d50cc9 | ||
|
|
f87506b490 | ||
|
|
f6f9a53ce5 | ||
|
|
61130ef54e | ||
|
|
e8bcbd59be | ||
|
|
d433ff7475 | ||
|
|
71957037d0 | ||
|
|
bcdcdbbd4e | ||
|
|
1ece13f5bc | ||
|
|
da70ba6ec7 | ||
|
|
a832f0e4d4 | ||
|
|
6ab4d005a0 | ||
|
|
18dc935038 | ||
|
|
bb3a534f75 | ||
|
|
649ba370c6 | ||
|
|
51732c5ce6 | ||
|
|
0ba7b46680 | ||
|
|
94dfbb7b9e | ||
|
|
b8b7512b35 | ||
|
|
aba6b773ce | ||
|
|
d88fb0d356 | ||
|
|
b8c6d55156 | ||
|
|
d9a5481db2 | ||
|
|
6a8bb42c0e | ||
|
|
2445f001ab | ||
|
|
c84fa9ac32 | ||
|
|
d44a11c44e | ||
|
|
b215cccda4 | ||
|
|
3eda488361 | ||
|
|
993a44de35 | ||
|
|
99465f99a1 | ||
|
|
d42de1fe90 | ||
|
|
d0284c62c0 | ||
|
|
d92f3b7904 | ||
|
|
3e1b5ada69 | ||
|
|
6156fb8154 | ||
|
|
f2753aed1e | ||
|
|
e33d826381 | ||
|
|
364aa4d324 | ||
|
|
455b332757 | ||
|
|
876b895645 | ||
|
|
daf7ed9fe6 | ||
|
|
3e365f83c9 | ||
|
|
40977ebdbe | ||
|
|
4a9058fc1c | ||
|
|
dab9a21608 | ||
|
|
a89c222399 | ||
|
|
1e356d2a45 | ||
|
|
eed794c2cf | ||
|
|
2f82e2015b | ||
|
|
ad599badb2 | ||
|
|
1e45c73f1d | ||
|
|
49c1fda2c1 | ||
|
|
32a31e464b | ||
|
|
cddb4167e4 | ||
|
|
65457d8df2 | ||
|
|
122a6cad43 | ||
|
|
4c0e841425 | ||
|
|
92f5839aec | ||
|
|
30475dae67 | ||
|
|
3d942bd503 | ||
|
|
f739520e52 | ||
|
|
0152603ddb | ||
|
|
aa06e0eead | ||
|
|
2fde9a285e | ||
|
|
b9f6eb6abb | ||
|
|
d77c4354a6 | ||
|
|
21860ddf85 | ||
|
|
2cffa22cc2 | ||
|
|
985ba9bb29 | ||
|
|
96f23f163d | ||
|
|
0e7d49991a | ||
|
|
3e635cf0fe | ||
|
|
1425c66c69 | ||
|
|
fc3b7cc75b | ||
|
|
2cc2dc6ceb | ||
|
|
7da8cedf66 | ||
|
|
2cf10668dd | ||
|
|
f1342b5ff2 | ||
|
|
a04b636a11 | ||
|
|
e4637ad295 | ||
|
|
3a1e43dabe | ||
|
|
10da1a7bfe | ||
|
|
37320ca862 | ||
|
|
2074a5e26f | ||
|
|
6b14cdbf06 | ||
|
|
3d6267da93 | ||
|
|
8def7f32af | ||
|
|
ac295de9f6 | ||
|
|
d412e0895e | ||
|
|
1f9e2b4fcb | ||
|
|
b13cff5a95 | ||
|
|
4c050744ad | ||
|
|
3b15c35a02 | ||
|
|
cd78dbf575 | ||
|
|
a258532d6a | ||
|
|
d2a87a1069 | ||
|
|
e07f24ea0a | ||
|
|
024e8dcfe2 | ||
|
|
4bb5aae40a | ||
|
|
dec94678ec | ||
|
|
1a49fc8251 | ||
|
|
29b0875960 | ||
|
|
976ccab1da | ||
|
|
02bbd37bb6 | ||
|
|
1bbbec7f2b | ||
|
|
0262f04913 | ||
|
|
fd1ec17d72 | ||
|
|
4bdad7fe0c | ||
|
|
74f73dc790 | ||
|
|
0527a8a76e | ||
|
|
c693eb8c02 | ||
|
|
3eb28c7fed | ||
|
|
618c4922b9 | ||
|
|
c81b15861d | ||
|
|
4a5832cccd | ||
|
|
4e47c4cbdf | ||
|
|
2af1520ecc | ||
|
|
cf397af3ab | ||
|
|
a1479705e2 | ||
|
|
175e23f110 | ||
|
|
9b7019ded2 | ||
|
|
7904aa9ba7 | ||
|
|
3fe6db589f | ||
|
|
625dccbbe4 | ||
|
|
e650771d16 | ||
|
|
3dad28b19d | ||
|
|
2b9fda985e | ||
|
|
ff8759409b | ||
|
|
0d9627c13f | ||
|
|
0179fd7e2a | ||
|
|
b8d0b0cf95 | ||
|
|
73c42bd452 | ||
|
|
290be7f5e0 | ||
|
|
0be1bb16e3 | ||
|
|
8605196390 | ||
|
|
460dd97537 | ||
|
|
1ec5984fad | ||
|
|
8260c2661d | ||
|
|
591c1dd405 | ||
|
|
0164cdfcc4 | ||
|
|
23a6cfff87 | ||
|
|
fdcdb90a5f | ||
|
|
0b7107401f | ||
|
|
06add65354 | ||
|
|
773d288497 | ||
|
|
fd4e8da938 | ||
|
|
271743b669 | ||
|
|
f4797caf37 | ||
|
|
62c9769c72 | ||
|
|
92d7bc1e2e | ||
|
|
99e4f96f63 | ||
|
|
94d8afe149 | ||
|
|
90ad59c63a | ||
|
|
c2273edfaa | ||
|
|
236283f4dd | ||
|
|
b66f12ff75 | ||
|
|
0403384beb | ||
|
|
7d7bc35869 | ||
|
|
48797038be | ||
|
|
d21792d04c | ||
|
|
8414e9d94f | ||
|
|
ab0243445d | ||
|
|
f2cf70b02f | ||
|
|
9ff417bba8 | ||
|
|
6b43d08a4d | ||
|
|
39bf7fe9b9 | ||
|
|
b5893c3b60 | ||
|
|
15657ba683 | ||
|
|
1aed9f0c67 | ||
|
|
17f1560310 | ||
|
|
41de0a3150 | ||
|
|
3e410be611 | ||
|
|
be75449e4a | ||
|
|
b1b05c8f6b | ||
|
|
a0e588c3de | ||
|
|
9e86a08327 | ||
|
|
ed007b5272 | ||
|
|
bdd5526244 | ||
|
|
ce35e30688 | ||
|
|
b40f72b1b0 | ||
|
|
31a6cf285b | ||
|
|
1842f4fda0 | ||
|
|
7a58902ab6 | ||
|
|
3cb91296ca | ||
|
|
130ed40580 | ||
|
|
94cb2caf0b | ||
|
|
917d060827 | ||
|
|
c58bc2be07 | ||
|
|
ff3356e2b4 | ||
|
|
79434c7be5 | ||
|
|
168dabc11f | ||
|
|
71ee263683 | ||
|
|
7239b9c289 | ||
|
|
84d36544e4 | ||
|
|
a71f57ae7f | ||
|
|
6e5bcd5e9a | ||
|
|
4131e563c3 | ||
|
|
613766dbca | ||
|
|
23b07d709e | ||
|
|
bbbdac2fd3 | ||
|
|
c614b7869c | ||
|
|
1f31e4a9d7 | ||
|
|
6cb6cd6b11 | ||
|
|
17ffec329f | ||
|
|
7a3927ef49 | ||
|
|
c1b0437c93 | ||
|
|
23e947f258 | ||
|
|
b8686d60b9 | ||
|
|
5d3de52da6 | ||
|
|
fee18fd183 | ||
|
|
3a43d461a3 | ||
|
|
5f409efad3 | ||
|
|
cc1f0796fe | ||
|
|
b1cd9b5412 | ||
|
|
42003a0247 | ||
|
|
d89c2bde94 | ||
|
|
3e6bdaafc6 | ||
|
|
93e4722a81 | ||
|
|
355ecb03da | ||
|
|
994c527c1d | ||
|
|
69b2e2045e | ||
|
|
7fde8bf5da | ||
|
|
637070a7ae | ||
|
|
7be75f72a7 | ||
|
|
e3fece41f3 | ||
|
|
87b0c8d7a2 | ||
|
|
ca9a711f22 | ||
|
|
27597bc994 | ||
|
|
776d75170d | ||
|
|
949780a9ad | ||
|
|
2f64cd9f05 | ||
|
|
185dfeb6fb | ||
|
|
68067d6258 | ||
|
|
b309deb0c4 | ||
|
|
e1dafdf0dd | ||
|
|
fd7b5f0f0f | ||
|
|
3b735d66fd | ||
|
|
3cd505ac8b | ||
|
|
81d856d78d | ||
|
|
11c1ad871a | ||
|
|
ffcb7db8ff | ||
|
|
02432e5109 | ||
|
|
a85bfb4432 | ||
|
|
17d85fdd85 | ||
|
|
5e0733d7f4 | ||
|
|
28d5287bd4 | ||
|
|
ef5097589a | ||
|
|
f287ec1fa4 | ||
|
|
28da93af9d | ||
|
|
0d5bdf5095 | ||
|
|
8dac3d7aa6 | ||
|
|
6951f31be7 | ||
|
|
630f85ec49 | ||
|
|
9ec4f2276f | ||
|
|
0a0c66541e | ||
|
|
bb4199620e | ||
|
|
8a83cd2cb8 | ||
|
|
fcfeaf568f | ||
|
|
25567ea434 | ||
|
|
1ab85d4c26 | ||
|
|
be68115099 | ||
|
|
ff0550b0fb | ||
|
|
1ab1e008fc | ||
|
|
6f30c72608 | ||
|
|
e927bc3d20 | ||
|
|
09ed7d1436 | ||
|
|
6fed657ea6 | ||
|
|
1555f8da0c | ||
|
|
68b28fc875 | ||
|
|
5d50d8cde8 | ||
|
|
9ff673d8be | ||
|
|
3e5a34fb56 | ||
|
|
64ee50d98c | ||
|
|
6a105c6f8f | ||
|
|
287aadc720 | ||
|
|
7229438a0b | ||
|
|
444af98a15 | ||
|
|
70c6fa1bbb | ||
|
|
d27f394b46 | ||
|
|
c8c98e13d0 | ||
|
|
9fcd722991 | ||
|
|
8080ecccc0 | ||
|
|
4b6fa9a1b1 | ||
|
|
0b6dbde7d4 | ||
|
|
fe4d63ba75 | ||
|
|
04bd3727ca | ||
|
|
7e6fcd03c2 | ||
|
|
fde8d6353b | ||
|
|
41b996168a | ||
|
|
b26f8fb900 | ||
|
|
bdbbc61d86 | ||
|
|
d64e200f2f | ||
|
|
71d54e2f9a | ||
|
|
ba3975993f | ||
|
|
050286ecd1 | ||
|
|
96c9a55c48 | ||
|
|
a20cbb2f1c | ||
|
|
2e957d7d9e | ||
|
|
18d0341056 | ||
|
|
a2bfed2433 | ||
|
|
67299338a8 | ||
|
|
971c049c5f | ||
|
|
bb7ba5ea49 | ||
|
|
747c986644 | ||
|
|
d88087c8ac | ||
|
|
968bdc330e | ||
|
|
7b49562c1d | ||
|
|
24ba60da47 | ||
|
|
ed5a07fbdb | ||
|
|
3d3a9b88e7 | ||
|
|
b2f9d5753e | ||
|
|
de46c7bd1d | ||
|
|
5b11d94f73 | ||
|
|
6f915a3739 | ||
|
|
d5c4b1bd01 | ||
|
|
1b1961db00 | ||
|
|
2dbd5be924 | ||
|
|
24c40d2dc6 | ||
|
|
5ef6feb996 | ||
|
|
85dad6f6f0 | ||
|
|
3cdcf0d9be | ||
|
|
b90a0a71e9 | ||
|
|
e62786a70f | ||
|
|
87722f2d28 | ||
|
|
a86276f18d | ||
|
|
f432a66016 | ||
|
|
9e2910d2ec | ||
|
|
46fbc1eb85 | ||
|
|
f397d3ab94 | ||
|
|
81a2da1f3f | ||
|
|
af3303c7b8 | ||
|
|
8ddd9ecf22 | ||
|
|
de72fe4fb9 | ||
|
|
108c8fc183 | ||
|
|
e86713e949 | ||
|
|
db9fba4cf2 | ||
|
|
926802d953 | ||
|
|
87f5e12b60 | ||
|
|
034eb83bae | ||
|
|
7f29275851 | ||
|
|
2fb3442800 | ||
|
|
462c857bba | ||
|
|
61aa589cda | ||
|
|
27704bf090 | ||
|
|
4c899861b1 | ||
|
|
0cff678c2d | ||
|
|
d18514d73c | ||
|
|
84736cac3f | ||
|
|
cda1cce495 | ||
|
|
fe3a0afd6c | ||
|
|
d533557324 | ||
|
|
d91755dff5 | ||
|
|
27d49a6093 | ||
|
|
ad1bdd9a3f | ||
|
|
4f2d7abc7e | ||
|
|
ef9de5e338 | ||
|
|
8b513e51b9 | ||
|
|
d43b38a23a | ||
|
|
0987c0f9d1 | ||
|
|
e2dde77023 | ||
|
|
104ac7caad | ||
|
|
8788dd3deb | ||
|
|
f64cc5e9cf | ||
|
|
75d1bbc6e8 | ||
|
|
b621aa7e65 | ||
|
|
9783108695 | ||
|
|
6ba32fe280 | ||
|
|
f75cc75bbc | ||
|
|
2109b65a8e | ||
|
|
a472751638 | ||
|
|
f08ed16f2a | ||
|
|
212d457a6a | ||
|
|
ac5f333766 | ||
|
|
640e4387c1 | ||
|
|
a16b19019f | ||
|
|
2443f576ac | ||
|
|
4fd7e44015 | ||
|
|
a0a3bda1c5 | ||
|
|
6bda7a3c73 | ||
|
|
69a7a714cd | ||
|
|
48e2e6468e | ||
|
|
4017ac780f | ||
|
|
a55cd1bb13 | ||
|
|
b34129e148 | ||
|
|
be25a68c9c | ||
|
|
468cd5e48e | ||
|
|
262c4e4aa5 | ||
|
|
4ccff6461f | ||
|
|
3bfa3ef389 | ||
|
|
5238971bcc | ||
|
|
2f5c904faf | ||
|
|
c62775813f | ||
|
|
f11b3754f0 | ||
|
|
3cbe0465e9 | ||
|
|
5048aea722 | ||
|
|
8d35339ab2 | ||
|
|
c3316a51e7 | ||
|
|
7ecf37064b | ||
|
|
18954f4f53 | ||
|
|
00bc245102 | ||
|
|
273cab9fdb | ||
|
|
84d0f0ec9e | ||
|
|
690f2a63e5 | ||
|
|
19cc020852 | ||
|
|
1d4353e6d1 | ||
|
|
a02b3f88d7 | ||
|
|
af44ca4c9f | ||
|
|
cf2b57bb96 | ||
|
|
d0a2288910 | ||
|
|
89c11afc21 | ||
|
|
da6ed580f1 | ||
|
|
1c33e297e7 | ||
|
|
0005534a95 | ||
|
|
11650f7c1a | ||
|
|
a222bb3f02 | ||
|
|
6361c5ef25 | ||
|
|
892e8a4508 | ||
|
|
8336d373f3 | ||
|
|
71072680a8 | ||
|
|
e13f105019 | ||
|
|
401577451e | ||
|
|
5c41ef1ee4 | ||
|
|
ad614830d1 | ||
|
|
3365837338 | ||
|
|
66ac2972d6 | ||
|
|
0d3e05880a | ||
|
|
997e71f3b7 | ||
|
|
b0fca4587d | ||
|
|
c10671768d | ||
|
|
91e8123679 | ||
|
|
417cf2f9ac | ||
|
|
277be7ab9b | ||
|
|
45f3459f59 | ||
|
|
98dad4a8ed | ||
|
|
1e5f19271b | ||
|
|
8abeeb4cf0 | ||
|
|
cae0cd9ead | ||
|
|
811574ae01 | ||
|
|
0ddecf7f8d | ||
|
|
5bcf50fb4d | ||
|
|
9f0654815d | ||
|
|
465e9f04f4 | ||
|
|
7c8cbfa4e2 | ||
|
|
4c79d13ff9 | ||
|
|
c815fad135 | ||
|
|
41f17d0378 | ||
|
|
50715ff2f7 | ||
|
|
ea9375d582 | ||
|
|
2cf3c93e5e | ||
|
|
ca888d8e41 | ||
|
|
2040b1c175 | ||
|
|
d451c18821 | ||
|
|
c13dfa4461 | ||
|
|
c2080ef75f | ||
|
|
6b252e8024 | ||
|
|
d06bd5f176 | ||
|
|
d55fc8da65 | ||
|
|
01184f2c5d | ||
|
|
cadc93c2f0 | ||
|
|
2600a51461 | ||
|
|
9e6a86ba8b | ||
|
|
c4430d63fa | ||
|
|
f3e1cfef33 | ||
|
|
cc3053719c | ||
|
|
cd6907e601 | ||
|
|
8fe00693bd | ||
|
|
fb90d1e015 | ||
|
|
4b44861e43 | ||
|
|
e42ca4ab5a | ||
|
|
5b97f6dd67 | ||
|
|
a9813ecb0a | ||
|
|
c7069a4049 | ||
|
|
5683ae0b3c | ||
|
|
1c8cf5ac98 | ||
|
|
ad7231ec56 | ||
|
|
7e4a607391 | ||
|
|
2d470d18e2 | ||
|
|
cb9a766c3b | ||
|
|
94b8184f7b | ||
|
|
270ea06f24 | ||
|
|
536afa92da | ||
|
|
11c48b3341 | ||
|
|
f514e172d7 | ||
|
|
1739769f87 | ||
|
|
7ccf643a34 | ||
|
|
edfaa650bf | ||
|
|
13c81afb79 | ||
|
|
c4f4ddcdc5 | ||
|
|
4f4d2ccff3 | ||
|
|
4db43517b7 | ||
|
|
41f38e040e | ||
|
|
3bdb50b1df | ||
|
|
0d1ca30ce3 | ||
|
|
d5697250e2 | ||
|
|
a54a327f22 | ||
|
|
7166482a46 | ||
|
|
839f509fef | ||
|
|
949b205d4f | ||
|
|
e6c3251067 | ||
|
|
77611a19c8 | ||
|
|
9b950cab4c | ||
|
|
8b9172ba94 | ||
|
|
d8a1d96e5c | ||
|
|
7b370d47ce | ||
|
|
434d9e3af6 | ||
|
|
42f95243d9 | ||
|
|
e77ca7124e | ||
|
|
5c51822996 | ||
|
|
12fe94cbff | ||
|
|
4c4cfbb60e | ||
|
|
416f94484d | ||
|
|
52e79446f6 | ||
|
|
63d4af8543 | ||
|
|
0c6e74911d | ||
|
|
c792bc25b6 | ||
|
|
e1f8b8b7b7 | ||
|
|
7339cb9cc9 | ||
|
|
5c99da6617 | ||
|
|
c22b54c898 | ||
|
|
8207a8f32a | ||
|
|
27cd80d508 | ||
|
|
f6af235f51 |
25
.github/workflows/dotnet.yml
vendored
Normal file
25
.github/workflows/dotnet.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: .NET
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 10.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
run: dotnet build --no-restore
|
||||
- name: Test
|
||||
run: dotnet test --no-build --verbosity normal
|
||||
31
.github/workflows/stale.yml
vendored
Normal file
31
.github/workflows/stale.yml
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
|
||||
#
|
||||
# You can adjust the behavior by modifying this file.
|
||||
# For more information, see:
|
||||
# https://github.com/actions/stale
|
||||
name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '33 20 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'No activity on this issue for 60 days. This issue will get closed if it receives no update within 14 more days.'
|
||||
stale-pr-message: 'No activity on this PR for 60 days. This PR will get closed if it receives no update within 14 more days.'
|
||||
close-issue-message: 'Closed for inactivity. Feel free to update this if the issue is still relevant.'
|
||||
close-pr-message: 'Closed for inactivity. Feel free to update this if the PR is still relevant.'
|
||||
exempt-issue-labels: 'Future'
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
days-before-close: 14
|
||||
@ -1,7 +0,0 @@
|
||||
language: csharp
|
||||
mono: none
|
||||
solution: CryptoExchange.Net.sln
|
||||
dotnet: 5.0.103
|
||||
script:
|
||||
- dotnet build CryptoExchange.Net/CryptoExchange.Net.csproj --framework "netstandard2.1"
|
||||
- dotnet test CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj
|
||||
@ -0,0 +1,530 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using ProtoBuf;
|
||||
using ProtoBuf.Meta;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.Protobuf
|
||||
{
|
||||
/// <summary>
|
||||
/// System.Text.Json message accessor
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public abstract class ProtobufMessageAccessor<
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
TIntermediateType> : IMessageAccessor
|
||||
#else
|
||||
public abstract class ProtobufMessageAccessor<TIntermediateType> : IMessageAccessor
|
||||
#endif
|
||||
{
|
||||
/// <summary>
|
||||
/// The intermediate deserialization object
|
||||
/// </summary>
|
||||
protected TIntermediateType? _intermediateType;
|
||||
/// <summary>
|
||||
/// Runtime type model
|
||||
/// </summary>
|
||||
protected RuntimeTypeModel _model;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool OriginalDataAvailable { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? Underlying => _intermediateType;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufMessageAccessor(RuntimeTypeModel model)
|
||||
{
|
||||
_model = model;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType(MessagePath path)
|
||||
{
|
||||
if (_intermediateType == null)
|
||||
throw new InvalidOperationException("Data not read");
|
||||
|
||||
object? value = _intermediateType;
|
||||
foreach (var step in path)
|
||||
{
|
||||
if (value == null)
|
||||
break;
|
||||
|
||||
if (step.Type == 0)
|
||||
{
|
||||
// array index
|
||||
}
|
||||
else if (step.Type == 1)
|
||||
{
|
||||
// property value
|
||||
#pragma warning disable IL2075 // Type is already annotated
|
||||
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
|
||||
#pragma warning restore
|
||||
}
|
||||
else
|
||||
{
|
||||
// property name
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
var valueType = value.GetType();
|
||||
if (valueType.IsArray)
|
||||
return NodeType.Array;
|
||||
|
||||
if (IsSimple(valueType))
|
||||
return NodeType.Value;
|
||||
|
||||
return NodeType.Object;
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2075:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public T? GetValue<T>(MessagePath path)
|
||||
{
|
||||
if (_intermediateType == null)
|
||||
throw new InvalidOperationException("Data not read");
|
||||
|
||||
object? value = _intermediateType;
|
||||
foreach(var step in path)
|
||||
{
|
||||
if (value == null)
|
||||
break;
|
||||
|
||||
if (step.Type == 0)
|
||||
{
|
||||
// array index
|
||||
}
|
||||
else if (step.Type == 1)
|
||||
{
|
||||
// property value
|
||||
#pragma warning disable IL2075 // Type is already annotated
|
||||
value = value.GetType().GetProperty(step.Property!)?.GetValue(value);
|
||||
#pragma warning restore
|
||||
}
|
||||
else
|
||||
{
|
||||
// property name
|
||||
}
|
||||
}
|
||||
|
||||
return (T?)value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T?[]? GetValues<T>(MessagePath path)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string GetOriginalString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Clear();
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public abstract CallResult<object> Deserialize(
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
Type type, MessagePath? path = null);
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public abstract CallResult<T> Deserialize<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
T>(MessagePath? path = null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System.Text.Json stream message accessor
|
||||
/// </summary>
|
||||
public class ProtobufStreamMessageAccessor<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IStreamMessageAccessor
|
||||
{
|
||||
private Stream? _stream;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufStreamMessageAccessor(RuntimeTypeModel model) : base(model)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override CallResult<object> Deserialize(
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
Type type, MessagePath? path = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _model.Deserialize(type, _stream);
|
||||
return new CallResult<object>(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override CallResult<T> Deserialize<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
T>(MessagePath? path = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _model.Deserialize<T>(_stream);
|
||||
return new CallResult<T>(result);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||
{
|
||||
if (bufferStream && stream is not MemoryStream)
|
||||
{
|
||||
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
|
||||
_stream = new MemoryStream();
|
||||
stream.CopyTo(_stream);
|
||||
_stream.Position = 0;
|
||||
}
|
||||
else if (bufferStream)
|
||||
{
|
||||
// We need to buffer the stream, and the current stream is seekable, store as is
|
||||
_stream = stream;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't need to buffer the stream, so don't bother keeping the reference
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_intermediateType = _model.Deserialize<TIntermediate>(_stream);
|
||||
IsValid = true;
|
||||
return Task.FromResult(CallResult.SuccessResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsValid = false;
|
||||
return Task.FromResult(new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString()
|
||||
{
|
||||
if (_stream is null)
|
||||
throw new NullReferenceException("Stream not initialized");
|
||||
|
||||
_stream.Position = 0;
|
||||
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
|
||||
return textReader.ReadToEnd();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear()
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
_intermediateType = default;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Protobuf byte message accessor
|
||||
/// </summary>
|
||||
public class ProtobufByteMessageAccessor<
|
||||
#if NET5_0_OR_GREATER
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
TIntermediate> : ProtobufMessageAccessor<TIntermediate>, IByteMessageAccessor
|
||||
{
|
||||
private ReadOnlyMemory<byte> _bytes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufByteMessageAccessor(RuntimeTypeModel model) : base(model)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override CallResult<object> Deserialize(
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
#endif
|
||||
Type type, MessagePath? path = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = new MemoryStream(_bytes.ToArray());
|
||||
stream.Position = 0;
|
||||
var result = _model.Deserialize(type, stream);
|
||||
return new CallResult<object>(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<object>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
#if NET5_0_OR_GREATER
|
||||
public override CallResult<T> Deserialize<
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
T>(MessagePath? path = null)
|
||||
#else
|
||||
public override CallResult<T> Deserialize<T>(MessagePath? path = null)
|
||||
#endif
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = _model.Deserialize<T>(_bytes);
|
||||
return new CallResult<T>(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<T>(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
_bytes = data;
|
||||
|
||||
try
|
||||
{
|
||||
_intermediateType = _model.Deserialize<TIntermediate>(data);
|
||||
IsValid = true;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError("Protobuf deserialization failed: " + ex.Message, ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString() =>
|
||||
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
||||
#if NETSTANDARD2_0
|
||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
||||
#else
|
||||
Encoding.UTF8.GetString(_bytes.Span);
|
||||
#endif
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear()
|
||||
{
|
||||
_bytes = null;
|
||||
_intermediateType = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using ProtoBuf.Meta;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.Protobuf
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ProtobufMessageSerializer : IByteMessageSerializer
|
||||
{
|
||||
private RuntimeTypeModel _model;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ProtobufMessageSerializer(RuntimeTypeModel model)
|
||||
{
|
||||
_model = model;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2092:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2095:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
#if NET5_0_OR_GREATER
|
||||
public byte[] Serialize<
|
||||
[DynamicallyAccessedMembers(
|
||||
#if NET8_0_OR_GREATER
|
||||
DynamicallyAccessedMemberTypes.NonPublicConstructors |
|
||||
DynamicallyAccessedMemberTypes.PublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicFields |
|
||||
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.PublicProperties |
|
||||
DynamicallyAccessedMemberTypes.NonPublicProperties |
|
||||
DynamicallyAccessedMemberTypes.PublicConstructors |
|
||||
#endif
|
||||
DynamicallyAccessedMemberTypes.PublicNestedTypes |
|
||||
DynamicallyAccessedMemberTypes.NonPublicMethods |
|
||||
DynamicallyAccessedMemberTypes.PublicMethods
|
||||
)]
|
||||
T>(T message)
|
||||
#else
|
||||
public byte[] Serialize<T>(T message)
|
||||
#endif
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
_model.Serialize(memoryStream, message);
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||
<PackageVersion>10.0.1</PackageVersion>
|
||||
<AssemblyVersion>10.0.1</AssemblyVersion>
|
||||
<FileVersion>10.0.1</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net/tree/master/CryptoExchange.Net.Protobuf</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\CryptoExchange.Net\Icon\icon.png" Pack="true" PackagePath="\" />
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
|
||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
128
CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.xml
Normal file
128
CryptoExchange.Net.Protobuf/CryptoExchange.Net.Protobuf.xml
Normal file
@ -0,0 +1,128 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>CryptoExchange.Net.Protobuf</name>
|
||||
</assembly>
|
||||
<members>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1">
|
||||
<summary>
|
||||
System.Text.Json message accessor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._intermediateType">
|
||||
<summary>
|
||||
The intermediate deserialization object
|
||||
</summary>
|
||||
</member>
|
||||
<member name="F:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1._model">
|
||||
<summary>
|
||||
Runtime type model
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.IsValid">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.OriginalDataAvailable">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Underlying">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetNodeType(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValue``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetValues``1(CryptoExchange.Net.Converters.MessageParsing.MessagePath)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.GetOriginalString">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Clear">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1">
|
||||
<summary>
|
||||
System.Text.Json stream message accessor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.OriginalDataAvailable">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Read(System.IO.Stream,System.Boolean)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.GetOriginalString">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufStreamMessageAccessor`1.Clear">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1">
|
||||
<summary>
|
||||
Protobuf byte message accessor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize(System.Type,System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Deserialize``1(System.Nullable{CryptoExchange.Net.Converters.MessageParsing.MessagePath})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Read(System.ReadOnlyMemory{System.Byte})">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.GetOriginalString">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="P:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.OriginalDataAvailable">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufByteMessageAccessor`1.Clear">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="T:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.#ctor(ProtoBuf.Meta.RuntimeTypeModel)">
|
||||
<summary>
|
||||
ctor
|
||||
</summary>
|
||||
</member>
|
||||
<member name="M:CryptoExchange.Net.Converters.Protobuf.ProtobufMessageSerializer.Serialize``1(``0)">
|
||||
<inheritdoc />
|
||||
</member>
|
||||
</members>
|
||||
</doc>
|
||||
52
CryptoExchange.Net.Protobuf/README.md
Normal file
52
CryptoExchange.Net.Protobuf/README.md
Normal file
@ -0,0 +1,52 @@
|
||||
#  CryptoExchange.Net.Proto
|
||||
|
||||
[](https://github.com/JKorf/CryptoExchange.Net/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/CryptoExchange.Net.Protobuf) 
|
||||
|
||||
Protobuf support for CryptoExchange.Net.
|
||||
|
||||
## Release notes
|
||||
* Version 10.0.1 - 16 Dec 2025
|
||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 10.0.0 - 16 Dec 2025
|
||||
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.13.0 - 10 Nov 2025
|
||||
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.12.0 - 03 Nov 2025
|
||||
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.11.1 - 30 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.11.0 - 30 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.10.0 - 15 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.9.0 - 06 Oct 2025
|
||||
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.8.0 - 30 Sep 2025
|
||||
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.7.0 - 01 Sep 2025
|
||||
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.6.0 - 25 Aug 2025
|
||||
* Updated CryptoExchange.Net version to 9.6.0
|
||||
|
||||
* Version 9.5.0 - 19 Aug 2025
|
||||
* Updated CryptoExchange.Net version to 9.5.0
|
||||
|
||||
* Version 9.4.0 - 04 Aug 2025
|
||||
* Updated CryptoExchange.Net to version 9.4.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
* Updated protobuf-net package version to 3.2.56
|
||||
|
||||
* Version 9.3.0 - 23 Jul 2025
|
||||
* Updated CryptoExchange.Net to version 9.3.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||
|
||||
* Version 9.2.0 - 14 Jul 2025
|
||||
* Initial release
|
||||
@ -1,9 +1,9 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
@ -24,8 +24,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result1 = await waiter1;
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(result2);
|
||||
Assert.That(result1);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -39,8 +39,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result1 = await waiter1;
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(result2);
|
||||
Assert.That(result1);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -55,14 +55,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
Assert.That(result1);
|
||||
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
|
||||
evnt.Set();
|
||||
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result2);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -75,13 +75,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
Assert.That(result1);
|
||||
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
evnt.Set();
|
||||
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result2);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -105,12 +105,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
for(var i = 1; i <= 10; i++)
|
||||
{
|
||||
evnt.Set();
|
||||
Assert.AreEqual(10 - i, waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||
await Task.Delay(1); // Wait for the continuation.
|
||||
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||
}
|
||||
|
||||
await resultsWaiter;
|
||||
|
||||
Assert.AreEqual(10, results.Count(r => r));
|
||||
Assert.That(10 == results.Count(r => r));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -124,7 +125,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.That(result1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -134,9 +135,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var waiter1 = evnt.WaitAsync(TimeSpan.FromMilliseconds(100));
|
||||
|
||||
var result1 = await waiter1;
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.False(result1);
|
||||
ClassicAssert.False(result1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,88 +1,11 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class BaseClientTests
|
||||
{
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", "")]
|
||||
[TestCase("test", null)]
|
||||
[TestCase("test", "")]
|
||||
[TestCase(null, "test")]
|
||||
[TestCase("", "test")]
|
||||
public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret)
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
// assert
|
||||
Assert.Throws(typeof(ArgumentException), () => new TestBaseClient(new RestClientOptions("") { ApiCredentials = new ApiCredentials(key, secret) }));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingLogOutput_Should_RedirectLogOutput()
|
||||
{
|
||||
// arrange
|
||||
var logger = new TestStringLogger();
|
||||
var client = new TestBaseClient(new RestClientOptions("")
|
||||
{
|
||||
LogWriters = new List<ILogger> { logger }
|
||||
});
|
||||
|
||||
// act
|
||||
client.Log(LogLevel.Information, "Test");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(string.IsNullOrEmpty(logger.GetLogs()));
|
||||
}
|
||||
|
||||
[TestCase(LogLevel.None, LogLevel.Error, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Warning, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.None, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Warning, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.Error, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Information, false)]
|
||||
[TestCase(LogLevel.Warning, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Information, true)]
|
||||
[TestCase(LogLevel.Information, LogLevel.Debug, false)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Error, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Warning, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Information, true)]
|
||||
[TestCase(LogLevel.Debug, LogLevel.Debug, true)]
|
||||
[TestCase(null, LogLevel.Error, true)]
|
||||
[TestCase(null, LogLevel.Warning, true)]
|
||||
[TestCase(null, LogLevel.Information, true)]
|
||||
[TestCase(null, LogLevel.Debug, true)]
|
||||
public void SettingLogLevel_Should_RestrictLogging(LogLevel? verbosity, LogLevel testVerbosity, bool expected)
|
||||
{
|
||||
// arrange
|
||||
var logger = new TestStringLogger();
|
||||
var client = new TestBaseClient(new RestClientOptions("")
|
||||
{
|
||||
LogWriters = new List<ILogger> { logger },
|
||||
LogLevel = verbosity
|
||||
});
|
||||
|
||||
// act
|
||||
client.Log(testVerbosity, "Test");
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(!string.IsNullOrEmpty(logger.GetLogs()), expected);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void DeserializingValidJson_Should_GiveSuccessfulResult()
|
||||
{
|
||||
@ -90,10 +13,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.Deserialize<object>("{\"testProperty\": 123}");
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -103,24 +26,24 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.Deserialize<object>("{\"testProperty\": 123");
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void FillingPathParameters_Should_ResultInValidUrl()
|
||||
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api", new[] { "path1/", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api", new[] { "path1/", "/path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com/api/", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
[TestCase("https://api.test.com", new[] { "test-path/test-path" }, "https://api.test.com/test-path/test-path")]
|
||||
[TestCase("https://api.test.com/", new[] { "test-path/test-path" }, "https://api.test.com/test-path/test-path")]
|
||||
public void AppendPathTests(string baseUrl, string[] path, string expected)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestBaseClient();
|
||||
|
||||
// act
|
||||
var result = client.FillParameters("http://test.api/{}/path/{}", "1", "test");
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result == "http://test.api/1/path/test");
|
||||
var result = baseUrl.AppendPath(path);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
184
CryptoExchange.Net.UnitTests/CallResultTests.cs
Normal file
184
CryptoExchange.Net.UnitTests/CallResultTests.cs
Normal file
@ -0,0 +1,184 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
internal class CallResultTests
|
||||
{
|
||||
[Test]
|
||||
public void TestBasicErrorCallResult()
|
||||
{
|
||||
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicSuccessCallResult()
|
||||
{
|
||||
var result = new CallResult(null);
|
||||
|
||||
ClassicAssert.IsNull(result.Error);
|
||||
Assert.That(result);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultError()
|
||||
{
|
||||
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError");
|
||||
ClassicAssert.IsNull(result.Data);
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultSuccess()
|
||||
{
|
||||
var result = new CallResult<object>(new object());
|
||||
|
||||
ClassicAssert.IsNull(result.Error);
|
||||
ClassicAssert.IsNotNull(result.Data);
|
||||
Assert.That(result);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultSuccessAs()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new TestObjectResult());
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
ClassicAssert.IsNull(asResult.Error);
|
||||
ClassicAssert.IsNotNull(asResult.Data);
|
||||
Assert.That(asResult.Data is not null);
|
||||
Assert.That(asResult);
|
||||
Assert.That(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultErrorAs()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.As<TestObject2>(default);
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCallResultErrorAsError()
|
||||
{
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultErrorAsError()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError", ErrorInfo.Unknown));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultSuccessAsError()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
HttpVersion.Version11,
|
||||
new HttpResponseMessage().Headers,
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new HttpRequestMessage().Headers,
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
|
||||
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
Assert.That(asResult.Error.ErrorCode == "TestError2");
|
||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWebCallResultSuccessAsSuccess()
|
||||
{
|
||||
var result = new WebCallResult<TestObjectResult>(
|
||||
System.Net.HttpStatusCode.OK,
|
||||
HttpVersion.Version11,
|
||||
new HttpResponseMessage().Headers,
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new HttpRequestMessage().Headers,
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
ClassicAssert.IsNull(asResult.Error);
|
||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||
ClassicAssert.IsNotNull(asResult.Data);
|
||||
Assert.That(asResult);
|
||||
Assert.That(asResult.Success);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestObjectResult
|
||||
{
|
||||
public TestObject2 InnerData;
|
||||
|
||||
public TestObjectResult()
|
||||
{
|
||||
InnerData = new TestObject2();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestObject2
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp5.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<packagereference Include="Microsoft.NET.Test.Sdk" Version="16.10.0"></packagereference>
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<packagereference Include="NUnit" Version="3.13.2"></packagereference>
|
||||
<packagereference Include="NUnit3TestAdapter" Version="3.17.0"></packagereference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.0.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -16,7 +16,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.ClampValue(min, max, input);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)]
|
||||
@ -30,10 +30,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
||||
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
||||
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
|
||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
|
||||
@ -48,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(5, 0.1563158, 0.15631)]
|
||||
@ -59,7 +60,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void RoundDownTests(int decimalPlaces, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.RoundDown(input, decimalPlaces);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(0.1234560000, "0.123456")]
|
||||
@ -67,7 +68,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void NormalizeTests(decimal input, string expected)
|
||||
{
|
||||
var result = ExchangeHelpers.Normalize(input);
|
||||
Assert.AreEqual(expected, result.ToString(CultureInfo.InvariantCulture));
|
||||
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("123", "BKR", 32, true, "BKRJK123")]
|
||||
[TestCase("123", "BKR", 32, false, "123")]
|
||||
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
|
||||
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
|
||||
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
|
||||
[TestCase(null, "BKR", 32, true, null)]
|
||||
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
|
||||
{
|
||||
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
|
||||
|
||||
if (expected != null)
|
||||
Assert.That(result, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
163
CryptoExchange.Net.UnitTests/OptionsTests.cs
Normal file
163
CryptoExchange.Net.UnitTests/OptionsTests.cs
Normal file
@ -0,0 +1,163 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class OptionsTests
|
||||
{
|
||||
[TearDown]
|
||||
public void Init()
|
||||
{
|
||||
TestClientOptions.Default = new TestClientOptions
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", "")]
|
||||
[TestCase("test", null)]
|
||||
[TestCase("test", "")]
|
||||
[TestCase(null, "test")]
|
||||
[TestCase("", "test")]
|
||||
public void SettingEmptyValuesForAPICredentials_Should_ThrowException(string key, string secret)
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
// assert
|
||||
Assert.Throws(typeof(ArgumentException),
|
||||
() => new RestExchangeOptions<TestEnvironment, ApiCredentials>() { ApiCredentials = new ApiCredentials(key, secret) });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBasicOptionsAreSet()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions
|
||||
{
|
||||
ApiCredentials = new ApiCredentials("123", "456"),
|
||||
ReceiveWindow = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
|
||||
Assert.That(options.ApiCredentials.Key == "123");
|
||||
Assert.That(options.ApiCredentials.Secret == "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApiOptionsAreSet()
|
||||
{
|
||||
// arrange, act
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("123", "456");
|
||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||
|
||||
// assert
|
||||
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
|
||||
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptions()
|
||||
{
|
||||
var client = new TestRestClient(options => {
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
});
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.That(authProvider1.GetKey() == "111");
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "333");
|
||||
Assert.That(authProvider2.GetSecret() == "444");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithDefault()
|
||||
{
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient();
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.That(authProvider1.GetKey() == "111");
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestClientUsesCorrectOptionsWithOverridingDefault()
|
||||
{
|
||||
TestClientOptions.Default.ApiCredentials = new ApiCredentials("123", "456");
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = new ApiCredentials("111", "222");
|
||||
|
||||
var client = new TestRestClient(options =>
|
||||
{
|
||||
options.Api1Options.ApiCredentials = new ApiCredentials("333", "444");
|
||||
options.Environment = new TestEnvironment("Test", "https://test.test");
|
||||
});
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.That(authProvider1.GetKey() == "333");
|
||||
Assert.That(authProvider1.GetSecret() == "444");
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestClientOptions: RestExchangeOptions<TestEnvironment, ApiCredentials>
|
||||
{
|
||||
/// <summary>
|
||||
/// Default options for the futures client
|
||||
/// </summary>
|
||||
public static TestClientOptions Default { get; set; } = new TestClientOptions()
|
||||
{
|
||||
Environment = new TestEnvironment("test", "https://test.com")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestClientOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
|
||||
|
||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||
|
||||
internal TestClientOptions Set(TestClientOptions targetOptions)
|
||||
{
|
||||
targetOptions = base.Set<TestClientOptions>(targetOptions);
|
||||
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
||||
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,18 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.RateLimiter;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using NUnit.Framework.Legacy;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.RateLimiting.Guards;
|
||||
using CryptoExchange.Net.RateLimiting.Filters;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@ -24,14 +25,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
var client = new TestRestClient();
|
||||
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
||||
client.SetResponse(JsonConvert.SerializeObject(expected), out _);
|
||||
client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = client.Api1.Request<TestObject>().Result;
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.IsTrue(TestHelpers.AreEqual(expected, result.Data));
|
||||
Assert.That(result.Success);
|
||||
Assert.That(TestHelpers.AreEqual(expected, result.Data));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -42,62 +43,60 @@ namespace CryptoExchange.Net.UnitTests
|
||||
client.SetResponse("{\"property\": 123", out _);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = client.Api1.Request<TestObject>().Result;
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void ReceivingErrorCode_Should_ResultInError()
|
||||
public async Task ReceivingErrorCode_Should_ResultInError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient();
|
||||
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
|
||||
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient();
|
||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
Assert.IsTrue(result.Error is ServerError);
|
||||
Assert.IsTrue(result.Error.Message.Contains("Invalid request"));
|
||||
Assert.IsTrue(result.Error.Message.Contains("123"));
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
Assert.That(result.Error is ServerError);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||
{
|
||||
// arrange
|
||||
var client = new ParseErrorTestRestClient();
|
||||
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
|
||||
|
||||
// act
|
||||
var result = client.Request<TestObject>().Result;
|
||||
var result = await client.Api2.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
Assert.IsTrue(result.Error is ServerError);
|
||||
Assert.IsTrue(result.Error.Code == 123);
|
||||
Assert.IsTrue(result.Error.Message == "Invalid request");
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
Assert.That(result.Error is ServerError);
|
||||
Assert.That(result.Error.ErrorCode == "123");
|
||||
Assert.That(result.Error.Message == "Invalid request");
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -105,20 +104,16 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
RateLimiters = new List<IRateLimiter>{new RateLimiterTotal(1, TimeSpan.FromSeconds(1))},
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail,
|
||||
RequestTimeout = TimeSpan.FromMinutes(1)
|
||||
});
|
||||
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.TimestampRecalculationInterval = TimeSpan.FromMinutes(10);
|
||||
options.Api1Options.OutputOriginalData = true;
|
||||
options.RequestTimeout = TimeSpan.FromMinutes(1);
|
||||
var client = new TestBaseClient(options);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(client.BaseAddress == "http://test.address.com/");
|
||||
Assert.IsTrue(client.RateLimiters.Count() == 1);
|
||||
Assert.IsTrue(client.RateLimitBehaviour == RateLimitingBehaviour.Fail);
|
||||
Assert.IsTrue(client.RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.TimestampRecalculationInterval == TimeSpan.FromMinutes(10));
|
||||
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.OutputOriginalData == true);
|
||||
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||
@ -132,16 +127,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
// arrange
|
||||
// act
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
});
|
||||
var client = new TestRestClient();
|
||||
|
||||
client.SetParameterPosition(new HttpMethod(method), pos);
|
||||
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
|
||||
|
||||
client.SetResponse("{}", out var request);
|
||||
|
||||
await client.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
|
||||
{
|
||||
{ "TestParam1", "Value1" },
|
||||
{ "TestParam2", 2 },
|
||||
@ -152,93 +144,239 @@ namespace CryptoExchange.Net.UnitTests
|
||||
});
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(request.Method, new HttpMethod(method));
|
||||
Assert.AreEqual(request.Content?.Contains("TestParam1") == true, pos == HttpMethodParameterPosition.InBody);
|
||||
Assert.AreEqual(request.Uri.ToString().Contains("TestParam1"), pos == HttpMethodParameterPosition.InUri);
|
||||
Assert.AreEqual(request.Content?.Contains("TestParam2") == true, pos == HttpMethodParameterPosition.InBody);
|
||||
Assert.AreEqual(request.Uri.ToString().Contains("TestParam2"), pos == HttpMethodParameterPosition.InUri);
|
||||
Assert.AreEqual(request.GetHeaders().First().Key, "TestHeader");
|
||||
Assert.IsTrue(request.GetHeaders().First().Value.Contains("123"));
|
||||
Assert.That(request.Method == new HttpMethod(method));
|
||||
Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody));
|
||||
Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
|
||||
Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody));
|
||||
Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
|
||||
Assert.That(request.GetHeaders().First().Key == "TestHeader");
|
||||
Assert.That(request.GetHeaders().First().Value.Contains("123"));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingRateLimitingBehaviourToFail_Should_FailLimitedRequests()
|
||||
|
||||
[TestCase(1, 0.1)]
|
||||
[TestCase(2, 0.1)]
|
||||
[TestCase(5, 1)]
|
||||
[TestCase(1, 2)]
|
||||
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
||||
|
||||
var triggered = false;
|
||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
||||
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Fail
|
||||
});
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
|
||||
|
||||
// act
|
||||
var result1 = client.Request<TestObject>().Result;
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
var result2 = client.Request<TestObject>().Result;
|
||||
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result1.Success);
|
||||
Assert.IsFalse(result2.Success);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(i == requests? triggered : !triggered);
|
||||
}
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingRateLimitingBehaviourToWait_Should_DelayLimitedRequests()
|
||||
[TestCase("/sapi/test1", true)]
|
||||
[TestCase("/sapi/test2", true)]
|
||||
[TestCase("/api/test1", false)]
|
||||
[TestCase("sapi/test1", true)]
|
||||
[TestCase("/sapi/", true)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiterTotal(1, TimeSpan.FromSeconds(1)) },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Wait
|
||||
});
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
|
||||
|
||||
// act
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result1 = client.Request<TestObject>().Result;
|
||||
client.SetResponse("{\"property\": 123}", out _); // reset response stream
|
||||
var result2 = client.Request<TestObject>().Result;
|
||||
sw.Stop();
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result1.Success);
|
||||
Assert.IsTrue(result2.Success);
|
||||
Assert.IsTrue(sw.ElapsedMilliseconds > 900, $"Actual: {sw.ElapsedMilliseconds}");
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void SettingApiKeyRateLimiter_Should_DelayRequestsFromSameKey()
|
||||
[TestCase("/sapi/", "/sapi/", true)]
|
||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("/sapi/test", "/sapi/test123", false)]
|
||||
[TestCase("/sapi/test", "/sapi/", false)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestRestClient(new RestClientOptions("")
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[TestCase(1, 0.1)]
|
||||
[TestCase(2, 0.1)]
|
||||
[TestCase(5, 1)]
|
||||
[TestCase(1, 2)]
|
||||
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/test"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
||||
|
||||
bool triggered = false;
|
||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
||||
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
RateLimiters = new List<IRateLimiter> { new RateLimiterAPIKey(1, TimeSpan.FromSeconds(1)) },
|
||||
RateLimitingBehaviour = RateLimitingBehaviour.Wait,
|
||||
LogLevel = LogLevel.Debug,
|
||||
ApiCredentials = new ApiCredentials("TestKey", "TestSecret")
|
||||
});
|
||||
client.SetResponse("{\"property\": 123}", out _);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(i == requests ? triggered : !triggered);
|
||||
}
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
[TestCase("/", false)]
|
||||
[TestCase("/sapi/test", true)]
|
||||
[TestCase("/sapi/test/123", false)]
|
||||
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
// act
|
||||
var sw = Stopwatch.StartNew();
|
||||
var result1 = client.Request<TestObject>().Result;
|
||||
client.SetKey("TestKey2", "TestSecret2"); // set to different key
|
||||
client.SetResponse("{\"property\": 123}", out _); // reset response stream
|
||||
var result2 = client.Request<TestObject>().Result;
|
||||
client.SetKey("TestKey", "TestSecret"); // set back to original key, should delay
|
||||
client.SetResponse("{\"property\": 123}", out _); // reset response stream
|
||||
var result3 = client.Request<TestObject>().Result;
|
||||
sw.Stop();
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
}
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result1.Success);
|
||||
Assert.IsTrue(result2.Success);
|
||||
Assert.IsTrue(result3.Success);
|
||||
Assert.IsTrue(sw.ElapsedMilliseconds > 900 && sw.ElapsedMilliseconds < 1900, $"Actual: {sw.ElapsedMilliseconds}");
|
||||
[TestCase("/", false)]
|
||||
[TestCase("/sapi/test", true)]
|
||||
[TestCase("/sapi/test2", true)]
|
||||
[TestCase("/sapi/test23", false)]
|
||||
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false)]
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("/sapi/test1", "/api/test2", true)]
|
||||
[TestCase("/", "/sapi/test2", true)]
|
||||
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test", true)]
|
||||
[TestCase("https://test2.com", "/sapi/test", "https://test.com", "/sapi/test", false)]
|
||||
[TestCase("https://test.com", "/sapi/test", "https://test2.com", "/sapi/test", false)]
|
||||
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test2", true)]
|
||||
public async Task HostRateLimiterBasics(string host1, string endpoint1, string host2, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[TestCase("https://test.com", "https://test.com", true)]
|
||||
[TestCase("https://test2.com", "https://test.com", false)]
|
||||
[TestCase("https://test.com", "https://test2.com", false)]
|
||||
public async Task ConnectionRateLimiterBasics(string host1, string host2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ConnectionRateLimiterCancel()
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,187 +1,234 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
//using CryptoExchange.Net.Objects;
|
||||
//using CryptoExchange.Net.Objects.Sockets;
|
||||
//using CryptoExchange.Net.Sockets;
|
||||
//using CryptoExchange.Net.Testing.Implementations;
|
||||
//using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||
//using Microsoft.Extensions.Logging;
|
||||
//using Moq;
|
||||
//using NUnit.Framework;
|
||||
//using NUnit.Framework.Legacy;
|
||||
//using System;
|
||||
//using System.Collections.Generic;
|
||||
//using System.Net.Sockets;
|
||||
//using System.Text.Json;
|
||||
//using System.Threading;
|
||||
//using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SocketClientTests
|
||||
{
|
||||
[TestCase]
|
||||
public void SettingOptions_Should_ResultInOptionsSet()
|
||||
{
|
||||
//arrange
|
||||
//act
|
||||
var client = new TestSocketClient(new SocketClientOptions("")
|
||||
{
|
||||
BaseAddress = "http://test.address.com",
|
||||
ReconnectInterval = TimeSpan.FromSeconds(6)
|
||||
});
|
||||
//namespace CryptoExchange.Net.UnitTests
|
||||
//{
|
||||
// [TestFixture]
|
||||
// public class SocketClientTests
|
||||
// {
|
||||
// [TestCase]
|
||||
// public void SettingOptions_Should_ResultInOptionsSet()
|
||||
// {
|
||||
// //arrange
|
||||
// //act
|
||||
// var client = new TestSocketClient(options =>
|
||||
// {
|
||||
// options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
||||
// options.SubOptions.MaxSocketConnections = 1;
|
||||
// });
|
||||
|
||||
// //assert
|
||||
// ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||
// Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
||||
// }
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(client.BaseAddress == "http://test.address.com/");
|
||||
Assert.IsTrue(client.ReconnectInterval.TotalSeconds == 6);
|
||||
}
|
||||
// [TestCase(true)]
|
||||
// [TestCase(false)]
|
||||
// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
|
||||
// {
|
||||
// //arrange
|
||||
// var client = new TestSocketClient();
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = canConnect;
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
|
||||
{
|
||||
//arrange
|
||||
var client = new TestSocketClient();
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = canConnect;
|
||||
// //act
|
||||
// var connectResult = client.SubClient.ConnectSocketSub(
|
||||
// new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||
|
||||
//act
|
||||
var connectResult = client.ConnectSocketSub(new SocketConnection(client, socket));
|
||||
// //assert
|
||||
// Assert.That(connectResult.Success == canConnect);
|
||||
// }
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(connectResult.Success == canConnect);
|
||||
}
|
||||
// [TestCase]
|
||||
// public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||
// {
|
||||
// // arrange
|
||||
// var client = new TestSocketClient(options => {
|
||||
// options.ReconnectInterval = TimeSpan.Zero;
|
||||
// });
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = true;
|
||||
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
// var rstEvent = new ManualResetEvent(false);
|
||||
// Dictionary<string, string> result = null;
|
||||
|
||||
[TestCase]
|
||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
JToken result = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
||||
{
|
||||
result = messageEvent.JsonData;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.ConnectSocketSub(sub);
|
||||
// client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
rstEvent.WaitOne(1000);
|
||||
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
// {
|
||||
// result = messageEvent.Data;
|
||||
// rstEvent.Set();
|
||||
// });
|
||||
// sub.AddSubscription(subObj);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue((int)result["property"] == 123);
|
||||
}
|
||||
// // act
|
||||
// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
||||
// rstEvent.WaitOne(1000);
|
||||
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug, OutputOriginalData = enabled });
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
string original = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, (messageEvent) =>
|
||||
{
|
||||
original = messageEvent.OriginalData;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.ConnectSocketSub(sub);
|
||||
// // assert
|
||||
// Assert.That(result["property"] == "123");
|
||||
// }
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
rstEvent.WaitOne(1000);
|
||||
// [TestCase(false)]
|
||||
// [TestCase(true)]
|
||||
// public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
// {
|
||||
// // arrange
|
||||
// var client = new TestSocketClient(options =>
|
||||
// {
|
||||
// options.ReconnectInterval = TimeSpan.Zero;
|
||||
// options.SubOptions.OutputOriginalData = enabled;
|
||||
// });
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = true;
|
||||
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
// var rstEvent = new ManualResetEvent(false);
|
||||
// string original = null;
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
|
||||
}
|
||||
// client.SubClient.ConnectSocketSub(sub);
|
||||
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
// {
|
||||
// original = messageEvent.OriginalData;
|
||||
// rstEvent.Set();
|
||||
// });
|
||||
// sub.AddSubscription(subObj);
|
||||
// var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
|
||||
|
||||
[TestCase]
|
||||
public void DisconnectedSocket_Should_Reconnect()
|
||||
{
|
||||
// arrange
|
||||
bool reconnected = false;
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
sub.ShouldReconnect = true;
|
||||
client.ConnectSocketSub(sub);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
sub.ConnectionRestored += (a) =>
|
||||
{
|
||||
reconnected = true;
|
||||
rstEvent.Set();
|
||||
};
|
||||
// // act
|
||||
// socket.InvokeMessage(msgToSend);
|
||||
// rstEvent.WaitOne(1000);
|
||||
|
||||
// act
|
||||
socket.InvokeClose();
|
||||
rstEvent.WaitOne(1000);
|
||||
// // assert
|
||||
// Assert.That(original == (enabled ? msgToSend : null));
|
||||
// }
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(reconnected);
|
||||
}
|
||||
// [TestCase()]
|
||||
// public void UnsubscribingStream_Should_CloseTheSocket()
|
||||
// {
|
||||
// // arrange
|
||||
// var client = new TestSocketClient(options =>
|
||||
// {
|
||||
// options.ReconnectInterval = TimeSpan.Zero;
|
||||
// });
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = true;
|
||||
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
// client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
[TestCase()]
|
||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = true;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
client.ConnectSocketSub(sub);
|
||||
var ups = new UpdateSubscription(sub, SocketSubscription.CreateForIdentifier(10, "Test", true, (e) => {}));
|
||||
// var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
// var ups = new UpdateSubscription(sub, subscription);
|
||||
// sub.AddSubscription(subscription);
|
||||
|
||||
// act
|
||||
client.UnsubscribeAsync(ups).Wait();
|
||||
// // act
|
||||
// client.UnsubscribeAsync(ups).Wait();
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(socket.Connected == false);
|
||||
}
|
||||
// // assert
|
||||
// Assert.That(socket.Connected == false);
|
||||
// }
|
||||
|
||||
[TestCase()]
|
||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket1 = client.CreateSocket();
|
||||
var socket2 = client.CreateSocket();
|
||||
socket1.CanConnect = true;
|
||||
socket2.CanConnect = true;
|
||||
var sub1 = new SocketConnection(client, socket1);
|
||||
var sub2 = new SocketConnection(client, socket2);
|
||||
client.ConnectSocketSub(sub1);
|
||||
client.ConnectSocketSub(sub2);
|
||||
// [TestCase()]
|
||||
// public void UnsubscribingAll_Should_CloseAllSockets()
|
||||
// {
|
||||
// // arrange
|
||||
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
// var socket1 = client.CreateSocket();
|
||||
// var socket2 = client.CreateSocket();
|
||||
// socket1.CanConnect = true;
|
||||
// socket2.CanConnect = true;
|
||||
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
// var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
// client.SubClient.ConnectSocketSub(sub1);
|
||||
// client.SubClient.ConnectSocketSub(sub2);
|
||||
// var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
// var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
|
||||
// act
|
||||
client.UnsubscribeAllAsync().Wait();
|
||||
// sub1.AddSubscription(subscription1);
|
||||
// sub2.AddSubscription(subscription2);
|
||||
// var ups1 = new UpdateSubscription(sub1, subscription1);
|
||||
// var ups2 = new UpdateSubscription(sub2, subscription2);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(socket1.Connected == false);
|
||||
Assert.IsTrue(socket2.Connected == false);
|
||||
}
|
||||
// // act
|
||||
// client.UnsubscribeAllAsync().Wait();
|
||||
|
||||
[TestCase()]
|
||||
public void FailingToConnectSocket_Should_ReturnError()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(new SocketClientOptions("") { ReconnectInterval = TimeSpan.Zero, LogLevel = LogLevel.Debug });
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = false;
|
||||
var sub = new SocketConnection(client, socket);
|
||||
// // assert
|
||||
// Assert.That(socket1.Connected == false);
|
||||
// Assert.That(socket2.Connected == false);
|
||||
// }
|
||||
|
||||
// act
|
||||
var connectResult = client.ConnectSocketSub(sub);
|
||||
// [TestCase()]
|
||||
// public void FailingToConnectSocket_Should_ReturnError()
|
||||
// {
|
||||
// // arrange
|
||||
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = false;
|
||||
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(connectResult.Success);
|
||||
}
|
||||
}
|
||||
}
|
||||
// // act
|
||||
// var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||
|
||||
// // assert
|
||||
// ClassicAssert.IsFalse(connectResult.Success);
|
||||
// }
|
||||
|
||||
// [TestCase()]
|
||||
// public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
|
||||
// {
|
||||
// // arrange
|
||||
// var channel = "trade_btcusd";
|
||||
// var client = new TestSocketClient(opt =>
|
||||
// {
|
||||
// opt.OutputOriginalData = true;
|
||||
// opt.SocketSubscriptionsCombineTarget = 1;
|
||||
// });
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = true;
|
||||
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||
|
||||
// // act
|
||||
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
|
||||
// await sub;
|
||||
|
||||
// // assert
|
||||
// ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
|
||||
// }
|
||||
|
||||
// [TestCase()]
|
||||
// public async Task SuccessResponse_Should_ConfirmSubscription()
|
||||
// {
|
||||
// // arrange
|
||||
// var channel = "trade_btcusd";
|
||||
// var client = new TestSocketClient(opt =>
|
||||
// {
|
||||
// opt.OutputOriginalData = true;
|
||||
// opt.SocketSubscriptionsCombineTarget = 1;
|
||||
// });
|
||||
// var socket = client.CreateSocket();
|
||||
// socket.CanConnect = true;
|
||||
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||
|
||||
// // act
|
||||
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
|
||||
// await sub;
|
||||
|
||||
// // assert
|
||||
// Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
@ -1,33 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.OrderBook;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SymbolOrderBookTests
|
||||
{
|
||||
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions("Test", true, false);
|
||||
private static readonly OrderBookOptions _defaultOrderBookOptions = new OrderBookOptions();
|
||||
|
||||
private class TestableSymbolOrderBook : SymbolOrderBook
|
||||
{
|
||||
public TestableSymbolOrderBook() : base("BTC/USD", defaultOrderBookOptions)
|
||||
public TestableSymbolOrderBook() : base(null, "Test", "Test", "BTC/USD")
|
||||
{
|
||||
Initialize(_defaultOrderBookOptions);
|
||||
}
|
||||
|
||||
public override void Dispose() {}
|
||||
|
||||
protected override Task<CallResult<bool>> DoResyncAsync()
|
||||
protected override Task<CallResult<bool>> DoResyncAsync(CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override Task<CallResult<UpdateSubscription>> DoStartAsync()
|
||||
protected override Task<CallResult<UpdateSubscription>> DoStartAsync(CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
@ -35,12 +38,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SetData(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
|
||||
{
|
||||
Status = OrderBookStatus.Synced;
|
||||
base.bids.Clear();
|
||||
base._bids.Clear();
|
||||
foreach (var bid in bids)
|
||||
base.bids.Add(bid.Price, bid);
|
||||
base.asks.Clear();
|
||||
base._bids.Add(bid.Price, bid);
|
||||
base._asks.Clear();
|
||||
foreach (var ask in asks)
|
||||
base.asks.Add(ask.Price, ask);
|
||||
base._asks.Add(ask.Price, ask);
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,31 +57,31 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry()
|
||||
{
|
||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||
Assert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.That(0m == symbolOrderBook.BestBid.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry()
|
||||
{
|
||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||
Assert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.That(0m == symbolOrderBook.BestBid.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries()
|
||||
{
|
||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||
Assert.IsNotNull(symbolOrderBook.BestOffers);
|
||||
Assert.IsNotNull(symbolOrderBook.BestOffers.Bid);
|
||||
Assert.IsNotNull(symbolOrderBook.BestOffers.Ask);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Quantity);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Bid);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Ask);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Quantity);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Quantity);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@ -101,12 +104,40 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid);
|
||||
var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask);
|
||||
|
||||
Assert.True(resultBids.Success);
|
||||
Assert.True(resultAsks.Success);
|
||||
Assert.AreEqual(1.05m, resultBids.Data);
|
||||
Assert.AreEqual(1.25m, resultAsks.Data);
|
||||
Assert.AreEqual(1.06666667m, resultBids2.Data);
|
||||
Assert.AreEqual(1.23333333m, resultAsks2.Data);
|
||||
Assert.That(resultBids.Success);
|
||||
Assert.That(resultAsks.Success);
|
||||
Assert.That(1.05m == resultBids.Data);
|
||||
Assert.That(1.25m == resultAsks.Data);
|
||||
Assert.That(1.06666667m == resultBids2.Data);
|
||||
Assert.That(1.23333333m == resultAsks2.Data);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void CalculateTradableAmount()
|
||||
{
|
||||
var orderbook = new TestableSymbolOrderBook();
|
||||
orderbook.SetData(new List<ISymbolOrderBookEntry>
|
||||
{
|
||||
new BookEntry{ Price = 1, Quantity = 1 },
|
||||
new BookEntry{ Price = 1.1m, Quantity = 1 },
|
||||
},
|
||||
new List<ISymbolOrderBookEntry>()
|
||||
{
|
||||
new BookEntry{ Price = 1.2m, Quantity = 1 },
|
||||
new BookEntry{ Price = 1.3m, Quantity = 1 },
|
||||
});
|
||||
|
||||
var resultBids = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Bid);
|
||||
var resultAsks = orderbook.CalculateTradableAmount(2, OrderBookEntryType.Ask);
|
||||
var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid);
|
||||
var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask);
|
||||
|
||||
Assert.That(resultBids.Success);
|
||||
Assert.That(resultAsks.Success);
|
||||
Assert.That(1.9m == resultBids.Data);
|
||||
Assert.That(1.61538462m == resultAsks.Data);
|
||||
Assert.That(1.4m == resultBids2.Data);
|
||||
Assert.That(1.23076923m == resultAsks2.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
440
CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs
Normal file
440
CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs
Normal file
@ -0,0 +1,440 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System.Text.Json;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class SystemTextJsonConverterTests
|
||||
{
|
||||
[TestCase("2021-05-12")]
|
||||
[TestCase("20210512")]
|
||||
[TestCase("210512")]
|
||||
[TestCase("1620777600.000")]
|
||||
[TestCase("1620777600000")]
|
||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
||||
[TestCase("0.000000", true)]
|
||||
[TestCase("0", true)]
|
||||
[TestCase("", true)]
|
||||
[TestCase(" ", true)]
|
||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
||||
{
|
||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
|
||||
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
||||
}
|
||||
|
||||
[TestCase(1620777600.000)]
|
||||
[TestCase(1620777600000d)]
|
||||
public void TestDateTimeConverterDouble(double input)
|
||||
{
|
||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
||||
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[TestCase(1620777600)]
|
||||
[TestCase(1620777600000)]
|
||||
[TestCase(1620777600000000)]
|
||||
[TestCase(1620777600000000000)]
|
||||
[TestCase(0, true)]
|
||||
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
||||
{
|
||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
||||
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
||||
}
|
||||
|
||||
[TestCase(1620777600)]
|
||||
[TestCase(1620777600.000)]
|
||||
public void TestDateTimeConverterFromSeconds(double input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromSeconds(input);
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToSeconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == 1620777600);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000)]
|
||||
[TestCase(1620777600000.000)]
|
||||
public void TestDateTimeConverterFromMilliseconds(double input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToMilliseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == 1620777600000);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000000)]
|
||||
public void TestDateTimeConverterFromMicroseconds(long input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToMicroseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == 1620777600000000);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000000000)]
|
||||
public void TestDateTimeConverterFromNanoseconds(long input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToNanoseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == 1620777600000000000);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public void TestDateTimeConverterNull()
|
||||
{
|
||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
|
||||
Assert.That(output.Time == null);
|
||||
}
|
||||
|
||||
[TestCase(TestEnum.One, "1")]
|
||||
[TestCase(TestEnum.Two, "2")]
|
||||
[TestCase(TestEnum.Three, "three")]
|
||||
[TestCase(TestEnum.Four, "Four")]
|
||||
[TestCase(null, null)]
|
||||
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
||||
{
|
||||
var output = EnumConverter.GetString(value);
|
||||
Assert.That(output == expected);
|
||||
}
|
||||
|
||||
[TestCase(TestEnum.One, "1")]
|
||||
[TestCase(TestEnum.Two, "2")]
|
||||
[TestCase(TestEnum.Three, "three")]
|
||||
[TestCase(TestEnum.Four, "Four")]
|
||||
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
||||
{
|
||||
var output = EnumConverter.GetString(value);
|
||||
Assert.That(output == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
[TestCase("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", null)]
|
||||
[TestCase(null, null)]
|
||||
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
[TestCase("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", TestEnum.One)]
|
||||
[TestCase(null, TestEnum.One)]
|
||||
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
[TestCase("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", null)]
|
||||
[TestCase(null, null)]
|
||||
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
||||
{
|
||||
var result = EnumConverter.ParseString<TestEnum>(value);
|
||||
Assert.That(result == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
[TestCase("y", true)]
|
||||
[TestCase("on", true)]
|
||||
[TestCase("-1", false)]
|
||||
[TestCase("0", false)]
|
||||
[TestCase("n", false)]
|
||||
[TestCase("no", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("off", false)]
|
||||
[TestCase("", null)]
|
||||
public void TestBoolConverter(string value, bool? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
[TestCase("y", true)]
|
||||
[TestCase("on", true)]
|
||||
[TestCase("-1", false)]
|
||||
[TestCase("0", false)]
|
||||
[TestCase("n", false)]
|
||||
[TestCase("no", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("off", false)]
|
||||
[TestCase("", false)]
|
||||
public void TestBoolConverterNotNullable(string value, bool expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", 1)]
|
||||
[TestCase("1.1", 1.1)]
|
||||
[TestCase("-1.1", -1.1)]
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", null)]
|
||||
[TestCase("null", null)]
|
||||
[TestCase("nan", null)]
|
||||
[TestCase("1E+2", 100)]
|
||||
[TestCase("1E-2", 0.01)]
|
||||
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
|
||||
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
|
||||
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||
public void TestDecimalConverterString(string value, decimal? expected)
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
|
||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue: expected));
|
||||
}
|
||||
|
||||
[TestCase("1", 1)]
|
||||
[TestCase("1.1", 1.1)]
|
||||
[TestCase("-1.1", -1.1)]
|
||||
[TestCase("null", null)]
|
||||
[TestCase("1E+2", 100)]
|
||||
[TestCase("1E-2", 0.01)]
|
||||
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
||||
public void TestDecimalConverterNumber(string value, decimal? expected)
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
|
||||
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
||||
}
|
||||
|
||||
[Test()]
|
||||
public void TestArrayConverter()
|
||||
{
|
||||
var data = new Test()
|
||||
{
|
||||
Prop1 = 2,
|
||||
Prop2 = null,
|
||||
Prop3 = "123",
|
||||
Prop3Again = "123",
|
||||
Prop4 = null,
|
||||
Prop5 = new Test2
|
||||
{
|
||||
Prop21 = 3,
|
||||
Prop22 = "456"
|
||||
},
|
||||
Prop6 = new Test3
|
||||
{
|
||||
Prop31 = 4,
|
||||
Prop32 = "789"
|
||||
},
|
||||
Prop7 = TestEnum.Two,
|
||||
TestInternal = new Test
|
||||
{
|
||||
Prop1 = 10
|
||||
},
|
||||
Prop8 = new Test3
|
||||
{
|
||||
Prop31 = 5,
|
||||
Prop32 = "101"
|
||||
},
|
||||
};
|
||||
|
||||
var options = new JsonSerializerOptions()
|
||||
{
|
||||
TypeInfoResolver = new SerializationContext()
|
||||
};
|
||||
var serialized = JsonSerializer.Serialize(data);
|
||||
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
||||
|
||||
Assert.That(deserialized.Prop1, Is.EqualTo(2));
|
||||
Assert.That(deserialized.Prop2, Is.Null);
|
||||
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
|
||||
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
|
||||
Assert.That(deserialized.Prop4, Is.Null);
|
||||
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
|
||||
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
|
||||
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
||||
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
||||
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
||||
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
|
||||
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
|
||||
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
|
||||
}
|
||||
|
||||
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
|
||||
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
|
||||
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
|
||||
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
|
||||
{
|
||||
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
|
||||
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
|
||||
|
||||
var serialized = JsonSerializer.Serialize(symbol);
|
||||
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
|
||||
|
||||
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
|
||||
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
|
||||
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
|
||||
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
|
||||
}
|
||||
|
||||
[TestCase(0.1, null, null)]
|
||||
[TestCase(0.1, 0.1, null)]
|
||||
[TestCase(0.1, 0.1, 0.1)]
|
||||
[TestCase(null, 0.1, null)]
|
||||
[TestCase(null, 0.1, 0.1)]
|
||||
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
|
||||
{
|
||||
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
|
||||
|
||||
var serialized = JsonSerializer.Serialize(symbol);
|
||||
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
|
||||
|
||||
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
|
||||
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
|
||||
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
|
||||
}
|
||||
}
|
||||
|
||||
public class STJDecimalObject
|
||||
{
|
||||
[JsonConverter(typeof(DecimalConverter))]
|
||||
[JsonPropertyName("test")]
|
||||
public decimal? Test { get; set; }
|
||||
}
|
||||
|
||||
public class STJTimeObject
|
||||
{
|
||||
[JsonConverter(typeof(DateTimeConverter))]
|
||||
[JsonPropertyName("time")]
|
||||
public DateTime? Time { get; set; }
|
||||
}
|
||||
|
||||
public class STJEnumObject
|
||||
{
|
||||
public TestEnum? Value { get; set; }
|
||||
}
|
||||
|
||||
public class NotNullableSTJEnumObject
|
||||
{
|
||||
public TestEnum Value { get; set; }
|
||||
}
|
||||
|
||||
public class STJBoolObject
|
||||
{
|
||||
public bool? Value { get; set; }
|
||||
}
|
||||
|
||||
public class NotNullableSTJBoolObject
|
||||
{
|
||||
public bool Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ArrayConverter<Test>))]
|
||||
record Test
|
||||
{
|
||||
[ArrayProperty(0)]
|
||||
public int Prop1 { get; set; }
|
||||
[ArrayProperty(1)]
|
||||
public int? Prop2 { get; set; }
|
||||
[ArrayProperty(2)]
|
||||
public string Prop3 { get; set; }
|
||||
[ArrayProperty(2)]
|
||||
public string Prop3Again { get; set; }
|
||||
[ArrayProperty(3)]
|
||||
public string Prop4 { get; set; }
|
||||
[ArrayProperty(4)]
|
||||
public Test2 Prop5 { get; set; }
|
||||
[ArrayProperty(5)]
|
||||
public Test3 Prop6 { get; set; }
|
||||
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
|
||||
public TestEnum? Prop7 { get; set; }
|
||||
[ArrayProperty(7)]
|
||||
public Test TestInternal { get; set; }
|
||||
[ArrayProperty(8), JsonConversion]
|
||||
public Test3 Prop8 { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ArrayConverter<Test2>))]
|
||||
record Test2
|
||||
{
|
||||
[ArrayProperty(0)]
|
||||
public int Prop21 { get; set; }
|
||||
[ArrayProperty(1)]
|
||||
public string Prop22 { get; set; }
|
||||
}
|
||||
|
||||
record Test3
|
||||
{
|
||||
[JsonPropertyName("prop31")]
|
||||
public int Prop31 { get; set; }
|
||||
[JsonPropertyName("prop32")]
|
||||
public string Prop32 { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(EnumConverter<TestEnum>))]
|
||||
public enum TestEnum
|
||||
{
|
||||
[Map("1")]
|
||||
One,
|
||||
[Map("2")]
|
||||
Two,
|
||||
[Map("three", "3")]
|
||||
Three,
|
||||
Four
|
||||
}
|
||||
|
||||
[JsonSerializable(typeof(Test))]
|
||||
[JsonSerializable(typeof(Test2))]
|
||||
[JsonSerializable(typeof(Test3))]
|
||||
[JsonSerializable(typeof(NotNullableSTJBoolObject))]
|
||||
[JsonSerializable(typeof(STJBoolObject))]
|
||||
[JsonSerializable(typeof(NotNullableSTJEnumObject))]
|
||||
[JsonSerializable(typeof(STJEnumObject))]
|
||||
[JsonSerializable(typeof(STJDecimalObject))]
|
||||
[JsonSerializable(typeof(STJTimeObject))]
|
||||
internal partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -1,56 +1,99 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
public class TestBaseClient: BaseClient
|
||||
{
|
||||
public TestBaseClient(): base("Test", new RestClientOptions("http://testurl.url"), null)
|
||||
public TestSubClient SubClient { get; }
|
||||
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
var options = new TestClientOptions();
|
||||
_logger = NullLogger.Instance;
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public TestBaseClient(RestClientOptions exchangeOptions) : base("Test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
||||
{
|
||||
_logger = NullLogger.Instance;
|
||||
Initialize(exchangeOptions);
|
||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
||||
}
|
||||
|
||||
public void Log(LogLevel verbosity, string data)
|
||||
{
|
||||
log.Write(verbosity, data);
|
||||
_logger.Log(verbosity, data);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSubClient : RestApiClient
|
||||
{
|
||||
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
|
||||
|
||||
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
||||
{
|
||||
}
|
||||
|
||||
public CallResult<T> Deserialize<T>(string data)
|
||||
{
|
||||
return Deserialize<T>(data, false);
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
|
||||
var accessor = CreateAccessor();
|
||||
var valid = accessor.Read(stream, true).Result;
|
||||
if (!valid)
|
||||
return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
|
||||
|
||||
var deserializeResult = accessor.Deserialize<T>();
|
||||
return deserializeResult;
|
||||
}
|
||||
|
||||
public string FillParameters(string path, params string[] values)
|
||||
{
|
||||
return FillPathParameter(path, values);
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||
public override TimeSpan? GetTimeOffset() => null;
|
||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public class TestAuthProvider : AuthenticationProvider
|
||||
{
|
||||
public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac];
|
||||
|
||||
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||
{
|
||||
}
|
||||
|
||||
public override Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization)
|
||||
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
|
||||
{
|
||||
return base.AddAuthenticationToHeaders(uri, method, parameters, signed, postParameters, arraySerialization);
|
||||
}
|
||||
|
||||
public string GetKey() => _credentials.Key;
|
||||
public string GetSecret() => _credentials.Secret;
|
||||
}
|
||||
|
||||
public override Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed, HttpMethodParameterPosition postParameters, ArrayParametersSerialization arraySerialization)
|
||||
{
|
||||
return base.AddAuthenticationToParameters(uri, method, parameters, signed, postParameters, arraySerialization);
|
||||
}
|
||||
public class TestEnvironment : TradeEnvironment
|
||||
{
|
||||
public string TestAddress { get; }
|
||||
|
||||
public override string Sign(string toSign)
|
||||
public TestEnvironment(string name, string url) : base(name)
|
||||
{
|
||||
return toSign;
|
||||
TestAddress = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestObject
|
||||
{
|
||||
[JsonProperty("other")]
|
||||
[JsonPropertyName("other")]
|
||||
public string StringData { get; set; }
|
||||
[JsonPropertyName("intData")]
|
||||
public int IntData { get; set; }
|
||||
[JsonPropertyName("decimalData")]
|
||||
public decimal DecimalData { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
@ -12,29 +11,34 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Net.Http.Headers;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestRestClient: RestClient
|
||||
public class TestRestClient: BaseRestClient
|
||||
{
|
||||
public TestRestClient() : base("Test", new RestClientOptions("http://testurl.url"), null)
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
|
||||
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
public TestRestClient(RestClientOptions exchangeOptions) : base("Test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
Initialize(options.Value);
|
||||
|
||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||
{
|
||||
ParameterPositions[method] = position;
|
||||
}
|
||||
|
||||
public void SetKey(string key, string secret)
|
||||
{
|
||||
SetAuthenticationProvider(new UnitTests.TestAuthProvider(new ApiCredentials(key, secret)));
|
||||
Api1 = new TestRestApi1Client(options.Value);
|
||||
Api2 = new TestRestApi2Client(options.Value);
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
@ -46,24 +50,33 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
var response = new Mock<IResponse>();
|
||||
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
||||
|
||||
var headers = new Dictionary<string, IEnumerable<string>>();
|
||||
var headers = new HttpRequestMessage().Headers;
|
||||
var request = new Mock<IRequest>();
|
||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, string, int>((method, uri, id) =>
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||
{
|
||||
request.Setup(a => a.Uri).Returns(new Uri(uri));
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
})
|
||||
.Returns(request.Object);
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||
{
|
||||
request.Setup(a => a.Uri).Returns(uri);
|
||||
request.Setup(a => a.Method).Returns(method);
|
||||
})
|
||||
.Returns(request.Object);
|
||||
requestObj = request.Object;
|
||||
}
|
||||
|
||||
@ -73,10 +86,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message);
|
||||
|
||||
var request = new Mock<IRequest>();
|
||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
|
||||
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
|
||||
@ -89,47 +109,126 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
|
||||
var response = new Mock<IResponse>();
|
||||
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
||||
|
||||
var headers = new Dictionary<string, IEnumerable<string>>();
|
||||
var headers = new List<KeyValuePair<string, string[]>>();
|
||||
var request = new Mock<IRequest>();
|
||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
||||
request.Setup(c => c.GetHeaders()).Returns(headers);
|
||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
|
||||
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
|
||||
|
||||
var factory = Mock.Get(RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<string>(), It.IsAny<int>()))
|
||||
.Callback<HttpMethod, string, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(new Uri(uri)))
|
||||
var factory = Mock.Get(Api1.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T:class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
||||
factory = Mock.Get(Api2.RequestFactory);
|
||||
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||
.Returns(request.Object);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestAuthProvider : AuthenticationProvider
|
||||
public class TestRestApi1Client : RestApiClient
|
||||
{
|
||||
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||
|
||||
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||
|
||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() });
|
||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||
{
|
||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
|
||||
{
|
||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
|
||||
}
|
||||
|
||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||
{
|
||||
ParameterPositions[method] = position;
|
||||
}
|
||||
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override TimeSyncInfo GetTimeSyncInfo()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestRestApi2Client : RestApiClient
|
||||
{
|
||||
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||
|
||||
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
||||
{
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||
{
|
||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||
}
|
||||
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override TimeSyncInfo GetTimeSyncInfo()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class TestError
|
||||
{
|
||||
[JsonPropertyName("errorCode")]
|
||||
public int ErrorCode { get; set; }
|
||||
[JsonPropertyName("errorMessage")]
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class ParseErrorTestRestClient: TestRestClient
|
||||
{
|
||||
public ParseErrorTestRestClient() { }
|
||||
public ParseErrorTestRestClient(RestClientOptions exchangeOptions) : base(exchangeOptions) { }
|
||||
|
||||
protected override Error ParseErrorResponse(JToken error)
|
||||
{
|
||||
return new ServerError((int)error["errorCode"], (string)error["errorMessage"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
internal class TestRestMessageHandler : JsonRestMessageHandler
|
||||
{
|
||||
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
||||
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
||||
|
||||
public override ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
||||
{
|
||||
var errorData = JsonSerializer.Deserialize<TestError>(responseStream);
|
||||
|
||||
return new ValueTask<Error>(new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,115 +0,0 @@
|
||||
using System;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocket: IWebsocket
|
||||
{
|
||||
public bool CanConnect { get; set; }
|
||||
public bool Connected { get; set; }
|
||||
|
||||
public event Action OnClose;
|
||||
public event Action<string> OnMessage;
|
||||
public event Action<Exception> OnError;
|
||||
public event Action OnOpen;
|
||||
|
||||
public int Id { get; }
|
||||
public bool ShouldReconnect { get; set; }
|
||||
public TimeSpan Timeout { get; set; }
|
||||
public Func<string, string> DataInterpreterString { get; set; }
|
||||
public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||
public DateTime? DisconnectTime { get; set; }
|
||||
public string Url { get; }
|
||||
public bool IsClosed => !Connected;
|
||||
public bool IsOpen => Connected;
|
||||
public bool PingConnection { get; set; }
|
||||
public TimeSpan PingInterval { get; set; }
|
||||
public SslProtocols SSLProtocols { get; set; }
|
||||
public Encoding Encoding { get; set; }
|
||||
|
||||
public int ConnectCalls { get; private set; }
|
||||
public bool Reconnecting { get; set; }
|
||||
public string Origin { get; set; }
|
||||
public int? RatelimitPerSecond { get; set; }
|
||||
|
||||
public double IncomingKbps => throw new NotImplementedException();
|
||||
|
||||
public static int lastId = 0;
|
||||
public static object lastIdLock = new object();
|
||||
|
||||
public TestSocket()
|
||||
{
|
||||
lock (lastIdLock)
|
||||
{
|
||||
Id = lastId + 1;
|
||||
lastId++;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> ConnectAsync()
|
||||
{
|
||||
Connected = CanConnect;
|
||||
ConnectCalls++;
|
||||
if (CanConnect)
|
||||
InvokeOpen();
|
||||
return Task.FromResult(CanConnect);
|
||||
}
|
||||
|
||||
public void Send(string data)
|
||||
{
|
||||
if(!Connected)
|
||||
throw new Exception("Socket not connected");
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
|
||||
public Task CloseAsync()
|
||||
{
|
||||
Connected = false;
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
OnClose?.Invoke();
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public void SetProxy(string host, int port)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
public void InvokeClose()
|
||||
{
|
||||
Connected = false;
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
|
||||
public void InvokeOpen()
|
||||
{
|
||||
OnOpen?.Invoke();
|
||||
}
|
||||
|
||||
public void InvokeMessage(string data)
|
||||
{
|
||||
OnMessage?.Invoke(data);
|
||||
}
|
||||
|
||||
public void SetProxy(ApiProxy proxy)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void InvokeError(Exception error)
|
||||
{
|
||||
OnError?.Invoke(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocketClient: SocketClient
|
||||
{
|
||||
public TestSocketClient() : this(new SocketClientOptions("http://testurl.url"))
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(SocketClientOptions exchangeOptions) : base("test", exchangeOptions, exchangeOptions.ApiCredentials == null ? null : new TestAuthProvider(exchangeOptions.ApiCredentials))
|
||||
{
|
||||
SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
||||
}
|
||||
|
||||
public TestSocket CreateSocket()
|
||||
{
|
||||
Mock.Get(SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<Log>(), It.IsAny<string>())).Returns(new TestSocket());
|
||||
return (TestSocket)CreateSocket(BaseAddress);
|
||||
}
|
||||
|
||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||
{
|
||||
return ConnectSocketAsync(sub).Result;
|
||||
}
|
||||
|
||||
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message,
|
||||
out CallResult<object> callResult)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool MessageMatchesHandler(JToken message, object request)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool MessageMatchesHandler(JToken message, string identifier)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected internal override Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection s)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription s)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestStringLogger : ILogger
|
||||
{
|
||||
StringBuilder _builder = new StringBuilder();
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) => null;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
||||
{
|
||||
_builder.AppendLine(formatter(state, exception));
|
||||
}
|
||||
|
||||
public string GetLogs()
|
||||
{
|
||||
return _builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
CryptoExchange.Net.UnitTests/TestSerializerContext.cs
Normal file
17
CryptoExchange.Net.UnitTests/TestSerializerContext.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[JsonSerializable(typeof(string))]
|
||||
[JsonSerializable(typeof(int))]
|
||||
[JsonSerializable(typeof(Dictionary<string, string>))]
|
||||
[JsonSerializable(typeof(IDictionary<string, string>))]
|
||||
[JsonSerializable(typeof(Dictionary<string, object>))]
|
||||
[JsonSerializable(typeof(IDictionary<string, object>))]
|
||||
[JsonSerializable(typeof(TestObject))]
|
||||
internal partial class TestSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,22 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27004.2008
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.32014.148
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net", "CryptoExchange.Net\CryptoExchange.Net.csproj", "{3762140C-7FF9-46E5-8EC3-BFB3FC7ADB9B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryptoExchange.Net.UnitTests", "CryptoExchange.Net.UnitTests\CryptoExchange.Net.UnitTests.csproj", "{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\BlazorClient\BlazorClient.csproj", "{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CryptoExchange.Net.Protobuf", "CryptoExchange.Net.Protobuf\CryptoExchange.Net.Protobuf.csproj", "{CC6A807A-9183-6F41-8EF1-8A70172B0E83}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -21,10 +31,31 @@ Global
|
||||
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FBFE1651-D43D-4D67-89B7-6C4AE9BA4496}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CC6A807A-9183-6F41-8EF1-8A70172B0E83}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
|
||||
EndGlobalSection
|
||||
|
||||
@ -1 +1,6 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||
|
||||
namespace System.Runtime.CompilerServices
|
||||
{
|
||||
internal static class IsExternalInit { }
|
||||
}
|
||||
@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
|
||||
/// <summary>
|
||||
/// Used for conversion in ArrayConverter
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class JsonConversionAttribute: Attribute
|
||||
{
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks property as optional
|
||||
/// </summary>
|
||||
public class JsonOptionalPropertyAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
25
CryptoExchange.Net/Attributes/MapAttribute.cs
Normal file
25
CryptoExchange.Net/Attributes/MapAttribute.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Attributes
|
||||
{
|
||||
/// <summary>
|
||||
/// Map a enum entry to string values
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class MapAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Values mapping to the enum entry
|
||||
/// </summary>
|
||||
public string[] Values { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="maps"></param>
|
||||
public MapAttribute(params string[] maps)
|
||||
{
|
||||
Values = maps;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
#if !NETSTANDARD2_1
|
||||
#if NETSTANDARD2_0
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
using System;
|
||||
|
||||
@ -1,128 +1,101 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Api credentials info
|
||||
/// Api credentials, used to sign requests accessing private endpoints
|
||||
/// </summary>
|
||||
public class ApiCredentials: IDisposable
|
||||
public class ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api key to authenticate requests
|
||||
/// The api key / label to authenticate requests
|
||||
/// </summary>
|
||||
public SecureString? Key { get; }
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api secret to authenticate requests
|
||||
/// The api secret or private key to authenticate requests
|
||||
/// </summary>
|
||||
public SecureString? Secret { get; }
|
||||
public string Secret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The private key to authenticate requests
|
||||
/// The api passphrase. Not needed on all exchanges
|
||||
/// </summary>
|
||||
public PrivateKey? PrivateKey { get; }
|
||||
public string? Pass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing a private key for authentication
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
/// <param name="privateKey">The private key used for signing</param>
|
||||
public ApiCredentials(PrivateKey privateKey)
|
||||
{
|
||||
PrivateKey = privateKey;
|
||||
}
|
||||
public ApiCredentialsType CredentialType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(SecureString key, SecureString secret)
|
||||
{
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing an api key and secret for authentication
|
||||
/// </summary>
|
||||
/// <param name="key">The api key used for identification</param>
|
||||
/// <param name="secret">The api secret used for signing</param>
|
||||
public ApiCredentials(string key, string secret)
|
||||
/// <param name="key">The api key / label used for identification</param>
|
||||
/// <param name="secret">The api secret or private key used for signing</param>
|
||||
/// <param name="pass">The api pass for the key. Not always needed</param>
|
||||
/// <param name="credentialType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
CredentialType = credentialType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
Pass = pass;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and secret generated by the server
|
||||
/// </summary>
|
||||
public static ApiCredentials HmacCredentials(string apiKey, string apiSecret, string? pass)
|
||||
{
|
||||
return new ApiCredentials(apiKey, apiSecret, pass, ApiCredentialsType.Hmac);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and an RSA private key in PEM format
|
||||
/// </summary>
|
||||
public static ApiCredentials RsaPemCredentials(string apiKey, string privateKey)
|
||||
{
|
||||
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaPem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and an RSA private key in XML format
|
||||
/// </summary>
|
||||
public static ApiCredentials RsaXmlCredentials(string apiKey, string privateKey)
|
||||
{
|
||||
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaXml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create API credentials using an API key and an Ed25519 private key
|
||||
/// </summary>
|
||||
public static ApiCredentials Ed25519Credentials(string apiKey, string privateKey)
|
||||
{
|
||||
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.Ed25519);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a key from a file
|
||||
/// </summary>
|
||||
public static string ReadFromFile(string path)
|
||||
{
|
||||
using var fileStream = File.OpenRead(path);
|
||||
using var streamReader = new StreamReader(fileStream);
|
||||
return streamReader.ReadToEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the credentials
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public ApiCredentials Copy()
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
if (PrivateKey == null)
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString());
|
||||
else
|
||||
return new ApiCredentials(PrivateKey!.Copy());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret
|
||||
/// </summary>
|
||||
/// <param name="inputStream">The stream containing the json data</param>
|
||||
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
|
||||
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
|
||||
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||
{
|
||||
using var reader = new StreamReader(inputStream, Encoding.UTF8, false, 512, true);
|
||||
|
||||
var stringData = reader.ReadToEnd();
|
||||
var jsonData = stringData.ToJToken();
|
||||
if(jsonData == null)
|
||||
throw new ArgumentException("Input stream not valid json data");
|
||||
|
||||
var key = TryGetValue(jsonData, identifierKey ?? "apiKey");
|
||||
var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret");
|
||||
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
||||
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
|
||||
inputStream.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try get the value of a key from a JToken
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
protected string? TryGetValue(JToken data, string key)
|
||||
{
|
||||
if (data[key] == null)
|
||||
return null;
|
||||
return (string) data[key]!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Key?.Dispose();
|
||||
Secret?.Dispose();
|
||||
PrivateKey?.Dispose();
|
||||
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
CryptoExchange.Net/Authentication/ApiCredentialsType.cs
Normal file
25
CryptoExchange.Net/Authentication/ApiCredentialsType.cs
Normal file
@ -0,0 +1,25 @@
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Credentials type
|
||||
/// </summary>
|
||||
public enum ApiCredentialsType
|
||||
{
|
||||
/// <summary>
|
||||
/// Hmac keys credentials
|
||||
/// </summary>
|
||||
Hmac,
|
||||
/// <summary>
|
||||
/// Rsa keys credentials in xml format
|
||||
/// </summary>
|
||||
RsaXml,
|
||||
/// <summary>
|
||||
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
||||
/// </summary>
|
||||
RsaPem,
|
||||
/// <summary>
|
||||
/// Ed25519 keys credentials
|
||||
/// </summary>
|
||||
Ed25519
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,16 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
#if NET8_0_OR_GREATER
|
||||
using NSec.Cryptography;
|
||||
#endif
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
@ -9,10 +19,38 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
public abstract class AuthenticationProvider
|
||||
{
|
||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||
|
||||
/// <summary>
|
||||
/// The provided credentials
|
||||
/// The supported credential types
|
||||
/// </summary>
|
||||
public ApiCredentials Credentials { get; }
|
||||
public abstract ApiCredentialsType[] SupportedCredentialTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Provided credentials
|
||||
/// </summary>
|
||||
protected internal readonly ApiCredentials _credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Byte representation of the secret
|
||||
/// </summary>
|
||||
protected byte[] _sBytes;
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
/// <summary>
|
||||
/// The Ed25519 private key
|
||||
/// </summary>
|
||||
protected Key? Ed25519Key;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Get the API key of the current credentials
|
||||
/// </summary>
|
||||
public string ApiKey => _credentials.Key!;
|
||||
/// <summary>
|
||||
/// Get the Passphrase of the current credentials
|
||||
/// </summary>
|
||||
public string? Pass => _credentials.Pass;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
@ -20,72 +58,471 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(ApiCredentials credentials)
|
||||
{
|
||||
Credentials = credentials;
|
||||
if (credentials.Key == null || credentials.Secret == null)
|
||||
throw new ArgumentException("ApiKey/Secret needed");
|
||||
|
||||
if (!SupportedCredentialTypes.Any(x => x == credentials.CredentialType))
|
||||
throw new ArgumentException($"Credential type {credentials.CredentialType} not supported");
|
||||
|
||||
if (credentials.CredentialType == ApiCredentialsType.Ed25519)
|
||||
{
|
||||
#if !NET8_0_OR_GREATER
|
||||
throw new ArgumentException($"Credential type Ed25519 only supported on Net8.0 or newer");
|
||||
#endif
|
||||
}
|
||||
|
||||
_credentials = credentials;
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add authentication to the parameter list based on the provided credentials
|
||||
/// Authenticate a request
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri the request is for</param>
|
||||
/// <param name="method">The HTTP method of the request</param>
|
||||
/// <param name="parameters">The provided parameters for the request</param>
|
||||
/// <param name="signed">Wether or not the request needs to be signed. If not typically the parameters list can just be returned</param>
|
||||
/// <param name="parameterPosition">Where parameters are placed, in the URI or in the request body</param>
|
||||
/// <param name="arraySerialization">How array parameters are serialized</param>
|
||||
/// <returns>Should return the original parameter list including any authentication parameters needed</returns>
|
||||
public virtual Dictionary<string, object> AddAuthenticationToParameters(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed,
|
||||
HttpMethodParameterPosition parameterPosition, ArrayParametersSerialization arraySerialization)
|
||||
{
|
||||
return parameters;
|
||||
}
|
||||
/// <param name="apiClient">The Api client sending the request</param>
|
||||
/// <param name="requestConfig">The request configuration</param>
|
||||
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Add authentication to the header dictionary based on the provided credentials
|
||||
/// SHA256 sign the data and return the bytes
|
||||
/// </summary>
|
||||
/// <param name="uri">The uri the request is for</param>
|
||||
/// <param name="method">The HTTP method of the request</param>
|
||||
/// <param name="parameters">The provided parameters for the request</param>
|
||||
/// <param name="signed">Wether or not the request needs to be signed. If not typically the parameters list can just be returned</param>
|
||||
/// <param name="parameterPosition">Where post parameters are placed, in the URI or in the request body</param>
|
||||
/// <param name="arraySerialization">How array parameters are serialized</param>
|
||||
/// <returns>Should return a dictionary containing any header key/value pairs needed for authenticating the request</returns>
|
||||
public virtual Dictionary<string, string> AddAuthenticationToHeaders(string uri, HttpMethod method, Dictionary<string, object> parameters, bool signed,
|
||||
HttpMethodParameterPosition parameterPosition, ArrayParametersSerialization arraySerialization)
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a string
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public virtual string Sign(string toSign)
|
||||
protected static byte[] SignSHA256Bytes(string data)
|
||||
{
|
||||
return toSign;
|
||||
using var encryptor = SHA256.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a byte array
|
||||
/// SHA256 sign the data and return the bytes
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public virtual byte[] Sign(byte[] toSign)
|
||||
protected static byte[] SignSHA256Bytes(byte[] data)
|
||||
{
|
||||
return toSign;
|
||||
using var encryptor = SHA256.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert byte array to hex
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA256(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA384(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA384Bytes(string data)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA384Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA512(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA512Bytes(string data)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA512Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignMD5(string data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignMD5(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignMD5Bytes(string data)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA256(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA384(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA512(string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA512(Encoding.UTF8.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA512(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
using var sha384 = SHA384.Create();
|
||||
var hash = sha384.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
using var sha512 = SHA512.Create();
|
||||
var hash = sha512.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ed25519 sign the data
|
||||
/// </summary>
|
||||
public string SignEd25519(string data, SignOutputType? outputType = null)
|
||||
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// Ed25519 sign the data
|
||||
/// </summary>
|
||||
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
#if NET8_0_OR_GREATER
|
||||
if (Ed25519Key == null)
|
||||
{
|
||||
var key = _credentials.Secret!
|
||||
.Replace("\n", "")
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.Replace("-----END PRIVATE KEY-----", "")
|
||||
.Trim();
|
||||
var keyBytes = Convert.FromBase64String(key);
|
||||
Ed25519Key = Key.Import(SignatureAlgorithm.Ed25519, keyBytes, KeyBlobFormat.PkixPrivateKey);
|
||||
}
|
||||
|
||||
var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
#else
|
||||
throw new InvalidOperationException();
|
||||
#endif
|
||||
}
|
||||
|
||||
private RSA CreateRSA()
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||
// Read from pem private key
|
||||
var key = _credentials.Secret!
|
||||
.Replace("\n", "")
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.Replace("-----END PRIVATE KEY-----", "")
|
||||
.Trim();
|
||||
rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(
|
||||
key)
|
||||
, out _);
|
||||
#else
|
||||
throw new Exception("Pem format not supported when running from .NetStandard2.0. Convert the private key to xml format.");
|
||||
#endif
|
||||
}
|
||||
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
|
||||
{
|
||||
// Read from xml private key format
|
||||
rsa.FromXmlString(_credentials.Secret!);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Invalid credentials type");
|
||||
}
|
||||
|
||||
return rsa;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert byte array to hex string
|
||||
/// </summary>
|
||||
/// <param name="buff"></param>
|
||||
/// <returns></returns>
|
||||
protected static string ByteToString(byte[] buff)
|
||||
protected static string BytesToHexString(byte[] buff)
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
return Convert.ToHexString(buff);
|
||||
#else
|
||||
var result = string.Empty;
|
||||
foreach (var t in buff)
|
||||
result += t.ToString("X2"); /* hex format */
|
||||
result += t.ToString("X2");
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert byte array to base64 string
|
||||
/// </summary>
|
||||
/// <param name="buff"></param>
|
||||
/// <returns></returns>
|
||||
protected static string BytesToBase64String(byte[] buff)
|
||||
{
|
||||
return Convert.ToBase64String(buff);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current timestamp including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected DateTime GetTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a string including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
|
||||
{
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the serialized request body
|
||||
/// </summary>
|
||||
/// <param name="serializer"></param>
|
||||
/// <param name="parameters"></param>
|
||||
/// <returns></returns>
|
||||
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
||||
{
|
||||
if (serializer is not IStringMessageSerializer stringSerializer)
|
||||
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
|
||||
|
||||
if (parameters?.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
return stringSerializer.Serialize(value);
|
||||
else
|
||||
return stringSerializer.Serialize(parameters);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(TApiCredentials credentials) : base(credentials)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using System.Security;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Private key info
|
||||
/// </summary>
|
||||
public class PrivateKey : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The private key
|
||||
/// </summary>
|
||||
public SecureString Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The private key's pass phrase
|
||||
/// </summary>
|
||||
public SecureString? Passphrase { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if the private key is encrypted or not
|
||||
/// </summary>
|
||||
public bool IsEncrypted { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
/// <param name="passphrase">The private key's passphrase</param>
|
||||
public PrivateKey(SecureString key, SecureString passphrase)
|
||||
{
|
||||
Key = key;
|
||||
Passphrase = passphrase;
|
||||
|
||||
IsEncrypted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
/// <param name="passphrase">The private key's passphrase</param>
|
||||
public PrivateKey(string key, string passphrase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(passphrase))
|
||||
throw new ArgumentException("Key and passphrase can't be null/empty");
|
||||
|
||||
var secureKey = new SecureString();
|
||||
foreach (var c in key)
|
||||
secureKey.AppendChar(c);
|
||||
secureKey.MakeReadOnly();
|
||||
Key = secureKey;
|
||||
|
||||
var securePassphrase = new SecureString();
|
||||
foreach (var c in passphrase)
|
||||
securePassphrase.AppendChar(c);
|
||||
securePassphrase.MakeReadOnly();
|
||||
Passphrase = securePassphrase;
|
||||
|
||||
IsEncrypted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an unencrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
public PrivateKey(SecureString key)
|
||||
{
|
||||
Key = key;
|
||||
|
||||
IsEncrypted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a private key providing an encrypted key information
|
||||
/// </summary>
|
||||
/// <param name="key">The private key used for signing</param>
|
||||
public PrivateKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
throw new ArgumentException("Key can't be null/empty");
|
||||
|
||||
Key = key.ToSecureString();
|
||||
|
||||
IsEncrypted = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy the private key
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public PrivateKey Copy()
|
||||
{
|
||||
if (Passphrase == null)
|
||||
return new PrivateKey(Key.GetString());
|
||||
else
|
||||
return new PrivateKey(Key.GetString(), Passphrase.GetString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Key?.Dispose();
|
||||
Passphrase?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
CryptoExchange.Net/Authentication/SignOutputType.cs
Normal file
17
CryptoExchange.Net/Authentication/SignOutputType.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Output string type
|
||||
/// </summary>
|
||||
public enum SignOutputType
|
||||
{
|
||||
/// <summary>
|
||||
/// Hex string
|
||||
/// </summary>
|
||||
Hex,
|
||||
/// <summary>
|
||||
/// Base64 string
|
||||
/// </summary>
|
||||
Base64
|
||||
}
|
||||
}
|
||||
@ -1,463 +0,0 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Logging;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// The base for all clients, websocket client and rest client
|
||||
/// </summary>
|
||||
public abstract class BaseClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The address of the client
|
||||
/// </summary>
|
||||
public string BaseAddress { get; }
|
||||
/// <summary>
|
||||
/// The name of the exchange the client is for
|
||||
/// </summary>
|
||||
public string ExchangeName { get; }
|
||||
/// <summary>
|
||||
/// The log object
|
||||
/// </summary>
|
||||
protected internal Log log;
|
||||
/// <summary>
|
||||
/// The api proxy
|
||||
/// </summary>
|
||||
protected ApiProxy? apiProxy;
|
||||
/// <summary>
|
||||
/// The authentication provider
|
||||
/// </summary>
|
||||
protected internal AuthenticationProvider? authProvider;
|
||||
/// <summary>
|
||||
/// Should check objects for missing properties based on the model and the received JSON
|
||||
/// </summary>
|
||||
public bool ShouldCheckObjects { get; set; }
|
||||
/// <summary>
|
||||
/// If true, the CallResult and DataEvent objects should also contain the originally received json data in the OriginalDaa property
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; private set; }
|
||||
/// <summary>
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
protected static int lastId;
|
||||
/// <summary>
|
||||
/// Lock for id generating
|
||||
/// </summary>
|
||||
protected static object idLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// A default serializer
|
||||
/// </summary>
|
||||
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Last id used
|
||||
/// </summary>
|
||||
public static int LastId => lastId;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="exchangeName">The name of the exchange this client is for</param>
|
||||
/// <param name="options">The options for this client</param>
|
||||
/// <param name="authenticationProvider">The authentication provider for this client (can be null if no credentials are provided)</param>
|
||||
protected BaseClient(string exchangeName, ClientOptions options, AuthenticationProvider? authenticationProvider)
|
||||
{
|
||||
log = new Log(exchangeName);
|
||||
authProvider = authenticationProvider;
|
||||
log.UpdateWriters(options.LogWriters);
|
||||
log.Level = options.LogLevel;
|
||||
|
||||
ExchangeName = exchangeName;
|
||||
OutputOriginalData = options.OutputOriginalData;
|
||||
BaseAddress = options.BaseAddress;
|
||||
apiProxy = options.Proxy;
|
||||
|
||||
log.Write(LogLevel.Debug, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {ExchangeName}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
ShouldCheckObjects = options.ShouldCheckObjects;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the authentication provider, can be used when manually setting the API credentials
|
||||
/// </summary>
|
||||
/// <param name="authenticationProvider"></param>
|
||||
protected void SetAuthenticationProvider(AuthenticationProvider authenticationProvider)
|
||||
{
|
||||
log.Write(LogLevel.Debug, "Setting api credentials");
|
||||
authProvider = authenticationProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the json data and returns a JToken, validating the input not being empty and being valid json
|
||||
/// </summary>
|
||||
/// <param name="data">The data to parse</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<JToken> ValidateJson(string data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
var info = "Empty data object received";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<JToken>(null, new DeserializeError(info, data));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<JToken>(JToken.Parse(data), null);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<JToken>(null, new DeserializeError(info, data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<JToken>(null, new DeserializeError(info, data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<JToken>(null, new DeserializeError(info, data));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a string into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="data">The data to deserialize</param>
|
||||
/// <param name="checkObject">Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug)</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(string data, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
var tokenResult = ValidateJson(data);
|
||||
if (!tokenResult)
|
||||
{
|
||||
log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>(default, tokenResult.Error);
|
||||
}
|
||||
|
||||
return Deserialize<T>(tokenResult.Data, checkObject, serializer, requestId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JToken into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="obj">The data to deserialize</param>
|
||||
/// <param name="checkObject">Whether or not the parsing should be checked for missing properties (will output data to the logging if log verbosity is Debug)</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(JToken obj, bool? checkObject = null, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
if (serializer == null)
|
||||
serializer = defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
if ((checkObject ?? ShouldCheckObjects)&& log.Level <= LogLevel.Debug)
|
||||
{
|
||||
// This checks the input JToken object against the class it is being serialized into and outputs any missing fields
|
||||
// in either the input or the class
|
||||
try
|
||||
{
|
||||
if (obj is JObject o)
|
||||
{
|
||||
CheckObject(typeof(T), o, requestId);
|
||||
}
|
||||
else if (obj is JArray j)
|
||||
{
|
||||
if (j.HasValues && j[0] is JObject jObject)
|
||||
CheckObject(typeof(T).GetElementType(), jObject, requestId);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Failed to check response data: " + (e.InnerException?.Message ?? e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer), null);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(default, new DeserializeError(info, obj));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(default, new DeserializeError(info, obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||
log.Write(LogLevel.Error, info);
|
||||
return new CallResult<T>(default, new DeserializeError(info, obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a stream into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="stream">The stream to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||
{
|
||||
if (serializer == null)
|
||||
serializer = defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
|
||||
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
||||
// in order to log/return the json data
|
||||
if (OutputOriginalData || log.Level <= LogLevel.Debug)
|
||||
{
|
||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: {data}");
|
||||
var result = Deserialize<T>(data, null, serializer, requestId);
|
||||
if(OutputOriginalData)
|
||||
result.OriginalData = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
||||
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
||||
using var jsonReader = new JsonTextReader(reader);
|
||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader), null);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
string data;
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Debug LogLevel]";
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||
return new CallResult<T>(default, new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
string data;
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Debug LogLevel]";
|
||||
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||
return new CallResult<T>(default, new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string data;
|
||||
if (stream.CanSeek) {
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
data = "[Data only available in Debug LogLevel]";
|
||||
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||
return new CallResult<T>(default, new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> ReadStreamAsync(Stream stream)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void CheckObject(Type type, JObject obj, int? requestId = null)
|
||||
{
|
||||
if (type == null)
|
||||
return;
|
||||
|
||||
if (type.GetCustomAttribute<JsonConverterAttribute>(true) != null)
|
||||
// If type has a custom JsonConverter we assume this will handle property mapping
|
||||
return;
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
return;
|
||||
|
||||
if (!obj.HasValues && type != typeof(object))
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Expected `{type.Name}`, but received object was empty");
|
||||
return;
|
||||
}
|
||||
|
||||
var isDif = false;
|
||||
var properties = new List<string>();
|
||||
var props = type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
|
||||
var ignore = prop.GetCustomAttributes(typeof(JsonIgnoreAttribute), false).FirstOrDefault();
|
||||
if (ignore != null)
|
||||
continue;
|
||||
|
||||
var propertyName = ((JsonPropertyAttribute?) attr)?.PropertyName;
|
||||
properties.Add(propertyName ?? prop.Name);
|
||||
}
|
||||
foreach (var token in obj)
|
||||
{
|
||||
var d = properties.FirstOrDefault(p => p == token.Key);
|
||||
if (d == null)
|
||||
{
|
||||
d = properties.SingleOrDefault(p => string.Equals(p, token.Key, StringComparison.CurrentCultureIgnoreCase));
|
||||
if (d == null)
|
||||
{
|
||||
if (!(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)))
|
||||
{
|
||||
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object doesn't have property `{token.Key}` expected in type `{type.Name}`");
|
||||
isDif = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
properties.Remove(d);
|
||||
|
||||
var propType = GetProperty(d, props)?.PropertyType;
|
||||
if (propType == null || token.Value == null)
|
||||
continue;
|
||||
if (!IsSimple(propType) && propType != typeof(DateTime))
|
||||
{
|
||||
if (propType.IsArray && token.Value.HasValues && ((JArray)token.Value).Any() && ((JArray)token.Value)[0] is JObject)
|
||||
CheckObject(propType.GetElementType()!, (JObject)token.Value[0]!, requestId);
|
||||
else if (token.Value is JObject o)
|
||||
CheckObject(propType, o, requestId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var propInfo = props.First(p => p.Name == prop ||
|
||||
((JsonPropertyAttribute)p.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault())?.PropertyName == prop);
|
||||
var optional = propInfo.GetCustomAttributes(typeof(JsonOptionalPropertyAttribute), false).FirstOrDefault();
|
||||
if (optional != null)
|
||||
continue;
|
||||
|
||||
isDif = true;
|
||||
log.Write(LogLevel.Warning, $"{(requestId != null ? $"[{requestId}] " : "")}Local object has property `{prop}` but was not found in received object of type `{type.Name}`");
|
||||
}
|
||||
|
||||
if (isDif)
|
||||
log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{ requestId}] " : "")}Returned data: " + obj);
|
||||
}
|
||||
|
||||
private static PropertyInfo? GetProperty(string name, IEnumerable<PropertyInfo> props)
|
||||
{
|
||||
foreach (var prop in props)
|
||||
{
|
||||
var attr = prop.GetCustomAttributes(typeof(JsonPropertyAttribute), false).FirstOrDefault();
|
||||
if (attr == null)
|
||||
{
|
||||
if (string.Equals(prop.Name, name, StringComparison.CurrentCultureIgnoreCase))
|
||||
return prop;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (((JsonPropertyAttribute)attr).PropertyName == name)
|
||||
return prop;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected int NextId()
|
||||
{
|
||||
lock (idLock)
|
||||
{
|
||||
lastId += 1;
|
||||
return lastId;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
|
||||
/// </summary>
|
||||
/// <param name="path">The total path string</param>
|
||||
/// <param name="values">The values to fill</param>
|
||||
/// <returns></returns>
|
||||
protected static string FillPathParameter(string path, params string[] values)
|
||||
{
|
||||
foreach (var value in values)
|
||||
{
|
||||
var index = path.IndexOf("{}", StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
path = path.Remove(index, 2);
|
||||
path = path.Insert(index, value);
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
authProvider?.Credentials?.Dispose();
|
||||
log.Write(LogLevel.Debug, "Disposing exchange client");
|
||||
}
|
||||
}
|
||||
}
|
||||
58
CryptoExchange.Net/Caching/MemoryCache.cs
Normal file
58
CryptoExchange.Net/Caching/MemoryCache.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.Caching
|
||||
{
|
||||
internal class MemoryCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _lock = new Lock();
|
||||
#else
|
||||
private readonly object _lock = new object();
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||
/// </summary>
|
||||
/// <param name="key">The key identifier</param>
|
||||
/// <param name="value">Cache value</param>
|
||||
public void Add(string key, object value)
|
||||
{
|
||||
var cacheItem = new CacheItem(DateTime.UtcNow, value);
|
||||
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a cached value
|
||||
/// </summary>
|
||||
/// <param name="key">The key identifier</param>
|
||||
/// <param name="maxAge">The max age of the cached entry</param>
|
||||
/// <returns>Cached value if it was in cache</returns>
|
||||
public object? Get(string key, TimeSpan maxAge)
|
||||
{
|
||||
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
|
||||
_cache.TryRemove(item.Key, out _);
|
||||
|
||||
_cache.TryGetValue(key, out CacheItem? value);
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
return value.Value;
|
||||
}
|
||||
|
||||
private class CacheItem
|
||||
{
|
||||
public DateTime CacheTime { get; }
|
||||
public object Value { get; }
|
||||
|
||||
public CacheItem(DateTime cacheTime, object value)
|
||||
{
|
||||
CacheTime = cacheTime;
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
CryptoExchange.Net/Clients/BaseApiClient.cs
Normal file
132
CryptoExchange.Net/Clients/BaseApiClient.cs
Normal file
@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base API for all API clients
|
||||
/// </summary>
|
||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Logger
|
||||
/// </summary>
|
||||
protected ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// If we are disposing
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
|
||||
/// <summary>
|
||||
/// The authentication provider for this API client. (null if no credentials are set)
|
||||
/// </summary>
|
||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The environment this client communicates to
|
||||
/// </summary>
|
||||
public string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Output the original string data along with the deserialized object
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Authenticated => ApiCredentials != null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public ApiCredentials? ApiCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
/// </summary>
|
||||
public ApiOptions ApiOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Client Options
|
||||
/// </summary>
|
||||
public ExchangeOptions ClientOptions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Mapping of a response code to known error types
|
||||
/// </summary>
|
||||
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiCredentials">Api credentials</param>
|
||||
/// <param name="clientOptions">Client options</param>
|
||||
/// <param name="apiOptions">Api options</param>
|
||||
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
ClientOptions = clientOptions;
|
||||
ApiOptions = apiOptions;
|
||||
OutputOriginalData = outputOriginalData;
|
||||
BaseAddress = baseAddress;
|
||||
ApiCredentials = apiCredentials?.Copy();
|
||||
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get error info for a response code
|
||||
/// </summary>
|
||||
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
|
||||
|
||||
/// <summary>
|
||||
/// Get error info for a response code
|
||||
/// </summary>
|
||||
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
ApiCredentials = credentials?.Copy();
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
|
||||
{
|
||||
ClientOptions.Proxy = options.Proxy;
|
||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||
|
||||
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||
if (ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
132
CryptoExchange.Net/Clients/BaseClient.cs
Normal file
132
CryptoExchange.Net/Clients/BaseClient.cs
Normal file
@ -0,0 +1,132 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// The base for all clients, websocket client and rest client
|
||||
/// </summary>
|
||||
public abstract class BaseClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the CryptoExchange.Net base library
|
||||
/// </summary>
|
||||
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the client implementation
|
||||
/// </summary>
|
||||
public Version ExchangeLibVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
lock(_versionLock)
|
||||
{
|
||||
if (_exchangeVersion == null)
|
||||
_exchangeVersion = GetType().Assembly.GetName().Version!;
|
||||
|
||||
return _exchangeVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the API the client is for
|
||||
/// </summary>
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
/// </summary>
|
||||
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
||||
|
||||
/// <summary>
|
||||
/// The log object
|
||||
/// </summary>
|
||||
protected internal ILogger _logger;
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _versionLock = new Lock();
|
||||
#else
|
||||
private readonly object _versionLock = new object();
|
||||
#endif
|
||||
private Version _exchangeVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
public ExchangeOptions ClientOptions { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
protected BaseClient(ILoggerFactory? logger, string exchange)
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
{
|
||||
Exchange = exchange;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the client with the specified options
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
protected virtual void Initialize(ExchangeOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
ClientOptions = options;
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||
/// </summary>
|
||||
/// <param name="credentials">The credentials to set</param>
|
||||
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
foreach (var apiClient in ApiClients)
|
||||
apiClient.SetApiCredentials(credentials);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register an API client
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The client</param>
|
||||
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
|
||||
{
|
||||
if (ClientOptions == null)
|
||||
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
||||
|
||||
ApiClients.Add(apiClient);
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the options delegate to a new options instance
|
||||
/// </summary>
|
||||
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
|
||||
{
|
||||
var opts = new T();
|
||||
del?.Invoke(opts);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public virtual void Dispose()
|
||||
{
|
||||
foreach (var client in ApiClients)
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
28
CryptoExchange.Net/Clients/BaseRestClient.cs
Normal file
28
CryptoExchange.Net/Clients/BaseRestClient.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System.Linq;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest client
|
||||
/// </summary>
|
||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||
{
|
||||
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
|
||||
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||
}
|
||||
}
|
||||
}
|
||||
137
CryptoExchange.Net/Clients/BaseSocketClient.cs
Normal file
137
CryptoExchange.Net/Clients/BaseSocketClient.cs
Normal file
@ -0,0 +1,137 @@
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base for socket client implementations
|
||||
/// </summary>
|
||||
public abstract class BaseSocketClient : BaseClient, ISocketClient
|
||||
{
|
||||
#region fields
|
||||
|
||||
/// <summary>
|
||||
/// If client is disposing
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||
/// <inheritdoc />
|
||||
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||
/// <inheritdoc />
|
||||
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||
|
||||
/// <inheritdoc />
|
||||
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">Logger factory</param>
|
||||
/// <param name="name">The name of the exchange this client is for</param>
|
||||
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||
{
|
||||
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
|
||||
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||
{
|
||||
foreach (var socket in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
||||
if (result)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe an update subscription
|
||||
/// </summary>
|
||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
|
||||
{
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
|
||||
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
||||
await subscription.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe all subscriptions
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAllAsync()
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
tasks.Add(client.UnsubscribeAllAsync());
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconnect all connections
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public virtual async Task ReconnectAsync()
|
||||
{
|
||||
_logger.ReconnectingAllConnections(CurrentConnections);
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
tasks.Add(client.ReconnectAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
public string GetSubscriptionsState()
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
|
||||
{
|
||||
result.AppendLine(client.GetSubscriptionsState());
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the state of all socket api clients
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
|
||||
{
|
||||
var result = new List<SocketApiClient.SocketApiClientState>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
result.Add(client.GetState());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
67
CryptoExchange.Net/Clients/CryptoBaseClient.cs
Normal file
67
CryptoExchange.Net/Clients/CryptoBaseClient.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base crypto client
|
||||
/// </summary>
|
||||
public class CryptoBaseClient : IDisposable
|
||||
{
|
||||
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Service provider
|
||||
/// </summary>
|
||||
protected readonly IServiceProvider? _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CryptoBaseClient() { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider"></param>
|
||||
public CryptoBaseClient(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_serviceCache = new Dictionary<Type, object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try get a client by type for the service collection
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T TryGet<T>(Func<T> createFunc)
|
||||
{
|
||||
var type = typeof(T);
|
||||
if (_serviceCache.TryGetValue(type, out var value))
|
||||
return (T)value;
|
||||
|
||||
if (_serviceProvider == null)
|
||||
{
|
||||
// Create with default options
|
||||
var createResult = createFunc();
|
||||
_serviceCache.Add(typeof(T), createResult!);
|
||||
return createResult;
|
||||
}
|
||||
|
||||
var result = _serviceProvider.GetService<T>()
|
||||
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
|
||||
_serviceCache.Add(type, result!);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_serviceCache.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
CryptoExchange.Net/Clients/CryptoRestClient.cs
Normal file
24
CryptoExchange.Net/Clients/CryptoRestClient.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CryptoRestClient()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider"></param>
|
||||
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
24
CryptoExchange.Net/Clients/CryptoSocketClient.cs
Normal file
24
CryptoExchange.Net/Clients/CryptoSocketClient.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CryptoSocketClient()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider"></param>
|
||||
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
777
CryptoExchange.Net/Clients/RestApiClient.cs
Normal file
777
CryptoExchange.Net/Clients/RestApiClient.cs
Normal file
@ -0,0 +1,777 @@
|
||||
using CryptoExchange.Net.Caching;
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Interfaces.Clients;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
using CryptoExchange.Net.Requests;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest API client for interacting with a REST API
|
||||
/// </summary>
|
||||
public abstract class RestApiClient : BaseApiClient, IRestApiClient
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract TimeSyncInfo? GetTimeSyncInfo();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract TimeSpan? GetTimeOffset();
|
||||
|
||||
/// <inheritdoc />
|
||||
public int TotalRequestsMade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Request body content type
|
||||
/// </summary>
|
||||
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
||||
|
||||
/// <summary>
|
||||
/// How to serialize array parameters when making requests
|
||||
/// </summary>
|
||||
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
||||
|
||||
/// <summary>
|
||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||
/// </summary>
|
||||
protected internal string RequestBodyEmptyContent = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// Request headers to be sent with each request
|
||||
/// </summary>
|
||||
protected Dictionary<string, string> StandardRequestHeaders { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Whether parameters need to be ordered
|
||||
/// </summary>
|
||||
protected internal bool OrderParameters { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Parameter order comparer
|
||||
/// </summary>
|
||||
protected IComparer<string> ParameterOrderComparer { get; } = new OrderedStringComparer();
|
||||
|
||||
/// <summary>
|
||||
/// Where to put the parameters for requests with different Http methods
|
||||
/// </summary>
|
||||
public Dictionary<HttpMethod, HttpMethodParameterPosition> ParameterPositions { get; set; } = new Dictionary<HttpMethod, HttpMethodParameterPosition>
|
||||
{
|
||||
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
|
||||
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
|
||||
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
|
||||
{ HttpMethod.Put, HttpMethodParameterPosition.InBody },
|
||||
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
||||
|
||||
/// <summary>
|
||||
/// Memory cache
|
||||
/// </summary>
|
||||
private readonly static MemoryCache _cache = new MemoryCache();
|
||||
|
||||
/// <summary>
|
||||
/// The message handler
|
||||
/// </summary>
|
||||
protected abstract IRestMessageHandler MessageHandler { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="httpClient">HttpClient to use</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="options">The base client options</param>
|
||||
/// <param name="apiOptions">The Api client options</param>
|
||||
public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions)
|
||||
: base(logger,
|
||||
apiOptions.OutputOriginalData ?? options.OutputOriginalData,
|
||||
apiOptions.ApiCredentials ?? options.ApiCredentials,
|
||||
baseAddress,
|
||||
options,
|
||||
apiOptions)
|
||||
{
|
||||
RequestFactory.Configure(options, httpClient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a message accessor instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected abstract IStreamMessageAccessor CreateAccessor();
|
||||
|
||||
/// <summary>
|
||||
/// Create a serializer instance
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected abstract IMessageSerializer CreateSerializer();
|
||||
|
||||
/// <summary>
|
||||
/// Send a request to the base address based on the request definition
|
||||
/// </summary>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="parameters">Request parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult> SendAsync(
|
||||
string baseAddress,
|
||||
RequestDefinition definition,
|
||||
ParameterCollection? parameters,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null)
|
||||
{
|
||||
var result = await SendAsync<object>(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
||||
return result.AsDataless();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a request to the base address based on the request definition
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Response type</typeparam>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="parameters">Request parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <returns></returns>
|
||||
protected virtual Task<WebCallResult<T>> SendAsync<T>(
|
||||
string baseAddress,
|
||||
RequestDefinition definition,
|
||||
ParameterCollection? parameters,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null,
|
||||
int? weightSingleLimiter = null,
|
||||
string? rateLimitKeySuffix = null)
|
||||
{
|
||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||
return SendAsync<T>(
|
||||
baseAddress,
|
||||
definition,
|
||||
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
|
||||
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
|
||||
cancellationToken,
|
||||
additionalHeaders,
|
||||
weight,
|
||||
weightSingleLimiter,
|
||||
rateLimitKeySuffix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a request to the base address based on the request definition
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Response type</typeparam>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="uriParameters">Request query parameters</param>
|
||||
/// <param name="bodyParameters">Request body parameters</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
|
||||
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
||||
string baseAddress,
|
||||
RequestDefinition definition,
|
||||
ParameterCollection? uriParameters,
|
||||
ParameterCollection? bodyParameters,
|
||||
CancellationToken cancellationToken,
|
||||
Dictionary<string, string>? additionalHeaders = null,
|
||||
int? weight = null,
|
||||
int? weightSingleLimiter = null,
|
||||
string? rateLimitKeySuffix = null)
|
||||
{
|
||||
var requestId = ExchangeHelpers.NextId();
|
||||
if (definition.Authenticated && AuthenticationProvider == null)
|
||||
{
|
||||
_logger.RestApiNoApiCredentials(requestId, definition.Path);
|
||||
return new WebCallResult<T>(new NoApiCredentialsError());
|
||||
}
|
||||
|
||||
string? cacheKey = null;
|
||||
if (ShouldCache(definition))
|
||||
{
|
||||
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
|
||||
_logger.CheckingCache(cacheKey);
|
||||
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
|
||||
if (cachedValue != null)
|
||||
{
|
||||
_logger.CacheHit(cacheKey);
|
||||
var original = (WebCallResult<T>)cachedValue;
|
||||
return original.Cached();
|
||||
}
|
||||
|
||||
_logger.CacheNotHit(cacheKey);
|
||||
}
|
||||
|
||||
int currentTry = 0;
|
||||
while (true)
|
||||
{
|
||||
currentTry++;
|
||||
|
||||
var error = await CheckTimeSync(requestId, definition).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
return new WebCallResult<T>(error);
|
||||
|
||||
error = await RateLimitAsync(
|
||||
baseAddress,
|
||||
requestId,
|
||||
definition,
|
||||
weight ?? definition.Weight,
|
||||
cancellationToken,
|
||||
weightSingleLimiter,
|
||||
rateLimitKeySuffix).ConfigureAwait(false);
|
||||
if (error != null)
|
||||
return new WebCallResult<T>(error);
|
||||
|
||||
var request = CreateRequest(
|
||||
requestId,
|
||||
baseAddress,
|
||||
definition,
|
||||
uriParameters,
|
||||
bodyParameters,
|
||||
additionalHeaders);
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
||||
TotalRequestsMade++;
|
||||
|
||||
var result = await GetResponseAsync2<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
||||
if (result.Error is not CancellationRequestedError)
|
||||
{
|
||||
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
|
||||
if (!result)
|
||||
{
|
||||
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.RestApiCancellationRequested(result.RequestId);
|
||||
}
|
||||
|
||||
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
||||
continue;
|
||||
|
||||
if (result.Success &&
|
||||
ShouldCache(definition))
|
||||
{
|
||||
_cache.Add(cacheKey!, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<Error?> CheckTimeSync(int requestId, RequestDefinition definition)
|
||||
{
|
||||
if (!definition.Authenticated)
|
||||
return null;
|
||||
|
||||
var syncTask = SyncTimeAsync();
|
||||
var timeSyncInfo = GetTimeSyncInfo();
|
||||
|
||||
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
|
||||
{
|
||||
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
|
||||
var syncTimeError = await syncTask.ConfigureAwait(false);
|
||||
if (syncTimeError != null)
|
||||
{
|
||||
_logger.RestApiFailedToSyncTime(requestId, syncTimeError!.ToString());
|
||||
return syncTimeError;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check rate limits for the request
|
||||
/// </summary>
|
||||
protected virtual async ValueTask<Error?> RateLimitAsync(
|
||||
string host,
|
||||
int requestId,
|
||||
RequestDefinition definition,
|
||||
int weight,
|
||||
CancellationToken cancellationToken,
|
||||
int? weightSingleLimiter = null,
|
||||
string? rateLimitKeySuffix = null)
|
||||
{
|
||||
// Rate limiting
|
||||
var requestWeight = weight;
|
||||
if (requestWeight != 0)
|
||||
{
|
||||
if (definition.RateLimitGate == null)
|
||||
throw new Exception("Ratelimit gate not set when request weight is not 0");
|
||||
|
||||
if (ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
return limitResult.Error!;
|
||||
}
|
||||
}
|
||||
|
||||
// Endpoint specific rate limiting
|
||||
if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
if (definition.RateLimitGate == null)
|
||||
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
|
||||
|
||||
if (ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
var singleRequestWeight = weightSingleLimiter ?? 1;
|
||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
|
||||
if (!limitResult)
|
||||
return limitResult.Error!;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a request object
|
||||
/// </summary>
|
||||
/// <param name="requestId">Id of the request</param>
|
||||
/// <param name="baseAddress">Host and schema</param>
|
||||
/// <param name="definition">Request definition</param>
|
||||
/// <param name="uriParameters">The query parameters of the request</param>
|
||||
/// <param name="bodyParameters">The body parameters of the request</param>
|
||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||
/// <returns></returns>
|
||||
protected virtual IRequest CreateRequest(
|
||||
int requestId,
|
||||
string baseAddress,
|
||||
RequestDefinition definition,
|
||||
ParameterCollection? uriParameters,
|
||||
ParameterCollection? bodyParameters,
|
||||
Dictionary<string, string>? additionalHeaders)
|
||||
{
|
||||
var requestConfiguration = new RestRequestConfiguration(
|
||||
definition,
|
||||
baseAddress,
|
||||
uriParameters == null ? null : CreateParameterDictionary(uriParameters),
|
||||
bodyParameters == null ? null : CreateParameterDictionary(bodyParameters),
|
||||
additionalHeaders,
|
||||
definition.ArraySerialization ?? ArraySerialization,
|
||||
definition.ParameterPosition ?? ParameterPositions[definition.Method],
|
||||
definition.RequestBodyFormat ?? RequestBodyFormat);
|
||||
|
||||
try
|
||||
{
|
||||
AuthenticationProvider?.ProcessRequest(this, requestConfiguration);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex);
|
||||
}
|
||||
|
||||
var queryString = requestConfiguration.GetQueryString(true);
|
||||
if (!string.IsNullOrEmpty(queryString) && !queryString.StartsWith("?"))
|
||||
queryString = $"?{queryString}";
|
||||
|
||||
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
|
||||
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
|
||||
request.Accept = MessageHandler.AcceptHeader;
|
||||
|
||||
if (requestConfiguration.Headers != null)
|
||||
{
|
||||
foreach (var header in requestConfiguration.Headers)
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
foreach (var header in StandardRequestHeaders)
|
||||
{
|
||||
// Only add it if it isn't overwritten
|
||||
requestConfiguration.Headers ??= new Dictionary<string, string>();
|
||||
if (!requestConfiguration.Headers.ContainsKey(header.Key))
|
||||
request.AddHeader(header.Key, header.Value);
|
||||
}
|
||||
|
||||
if (requestConfiguration.ParameterPosition == HttpMethodParameterPosition.InBody)
|
||||
{
|
||||
var contentType = requestConfiguration.BodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||
var bodyContent = requestConfiguration.GetBodyContent();
|
||||
if (bodyContent != null)
|
||||
{
|
||||
request.SetContent(bodyContent, contentType);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (requestConfiguration.BodyParameters != null && requestConfiguration.BodyParameters.Count != 0)
|
||||
WriteParamBody(request, requestConfiguration.BodyParameters, contentType);
|
||||
else
|
||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the request and returns the result deserialized into the type parameter class
|
||||
/// </summary>
|
||||
/// <param name="requestDefinition">The request definition</param>
|
||||
/// <param name="request">The request object to execute</param>
|
||||
/// <param name="gate">The ratelimit gate used</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync2<T>(
|
||||
RequestDefinition requestDefinition,
|
||||
IRequest request,
|
||||
IRateLimitGate? gate,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
Stream? responseStream = null;
|
||||
IResponse? response = null;
|
||||
|
||||
try
|
||||
{
|
||||
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
string? originalData = null;
|
||||
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
|
||||
if (outputOriginalData || MessageHandler.RequiresSeekableStream)
|
||||
{
|
||||
// If we want to return the original string data from the stream, but still want to process it
|
||||
// we'll need to copy it as the stream isn't seekable, and thus we can only read it once
|
||||
var memoryStream = new MemoryStream();
|
||||
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
|
||||
using var reader = new StreamReader(memoryStream, Encoding.UTF8, false, 4096, true);
|
||||
if (outputOriginalData)
|
||||
{
|
||||
memoryStream.Position = 0;
|
||||
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
_logger.RestApiReceivedResponse(request.RequestId, originalData);
|
||||
}
|
||||
|
||||
// Continue processing from the memory stream since the response stream is already read and we can't seek it
|
||||
responseStream.Close();
|
||||
memoryStream.Position = 0;
|
||||
responseStream = memoryStream;
|
||||
}
|
||||
|
||||
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
|
||||
{
|
||||
// If the response status is not success it is an error by definition
|
||||
|
||||
Error error;
|
||||
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
||||
{
|
||||
// Specifically handle rate limit errors
|
||||
var rateError = await MessageHandler.ParseErrorRateLimitResponse(
|
||||
(int)response.StatusCode,
|
||||
response.ResponseHeaders,
|
||||
responseStream).ConfigureAwait(false);
|
||||
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
|
||||
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
error = rateError;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string
|
||||
|
||||
try
|
||||
{
|
||||
error = await MessageHandler.ParseErrorResponse(
|
||||
(int)response.StatusCode,
|
||||
response.ResponseHeaders,
|
||||
responseStream).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message);
|
||||
var errorResult = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, errorResult);
|
||||
}
|
||||
}
|
||||
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(object))
|
||||
// Success status code and expected empty response, assume it's correct
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||
|
||||
// Data response received, inspect the message and check if it is an error or not
|
||||
var parsedError = await MessageHandler.CheckForErrorResponse(
|
||||
requestDefinition,
|
||||
response.ResponseHeaders,
|
||||
responseStream).ConfigureAwait(false);
|
||||
if (parsedError != null)
|
||||
{
|
||||
if (parsedError is ServerRateLimitError rateError)
|
||||
{
|
||||
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
|
||||
{
|
||||
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
|
||||
await gate.SetRetryAfterGuardAsync(rateError.RetryAfter.Value).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Success status code, but TryParseError determined it was an error response
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
||||
}
|
||||
|
||||
if (MessageHandler.RequiresSeekableStream)
|
||||
// Reset stream read position as it might not be at the start if `CheckForErrorResponse` has read from it
|
||||
responseStream.Position = 0;
|
||||
|
||||
// Try deserialization into the expected type
|
||||
var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync<T>(responseStream, cancellationToken).ConfigureAwait(false);
|
||||
if (deserializeError != null)
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError); ;
|
||||
|
||||
try
|
||||
{
|
||||
// Check the deserialized response to see if it's an error or not
|
||||
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
|
||||
if (responseError != null)
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message);
|
||||
var error = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, error);
|
||||
}
|
||||
|
||||
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, null);
|
||||
}
|
||||
catch (HttpRequestException requestException)
|
||||
{
|
||||
// Request exception, can't reach server for instance
|
||||
var error = new WebError(requestException.Message, requestException);
|
||||
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
}
|
||||
catch (OperationCanceledException canceledException)
|
||||
{
|
||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||
{
|
||||
// Cancellation token canceled by caller
|
||||
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Request timed out
|
||||
var error = new WebError($"Request timed out", exception: canceledException);
|
||||
error.ErrorType = ErrorType.Timeout;
|
||||
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
}
|
||||
}
|
||||
catch (ArgumentException argumentException)
|
||||
{
|
||||
if (argumentException.Message.StartsWith("Only HTTP/"))
|
||||
{
|
||||
// Unsupported HTTP version error .net framework
|
||||
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
|
||||
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (NotSupportedException notSupportedException)
|
||||
{
|
||||
if (notSupportedException.Message.StartsWith("Request version value must be one of"))
|
||||
{
|
||||
// Unsupported HTTP version error dotnet code
|
||||
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
|
||||
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
responseStream?.Close();
|
||||
response?.Close();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
|
||||
/// Note that this is always called; even when the request might be successful
|
||||
/// </summary>
|
||||
/// <typeparam name="T">WebCallResult type parameter</typeparam>
|
||||
/// <param name="gate">The rate limit gate the call used</param>
|
||||
/// <param name="callResult">The result of the call</param>
|
||||
/// <param name="tries">The current try number</param>
|
||||
/// <returns>True if call should retry, false if the call should return</returns>
|
||||
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
|
||||
{
|
||||
if (tries >= 2)
|
||||
// Only retry once
|
||||
return false;
|
||||
|
||||
if (callResult.Error is ServerRateLimitError
|
||||
&& ClientOptions.RateLimiterEnabled
|
||||
&& ClientOptions.RateLimitingBehaviour != RateLimitingBehaviour.Fail
|
||||
&& gate != null)
|
||||
{
|
||||
var retryTime = await gate.GetRetryAfterTime().ConfigureAwait(false);
|
||||
if (retryTime == null)
|
||||
return false;
|
||||
|
||||
if (retryTime.Value - DateTime.UtcNow < TimeSpan.FromSeconds(60))
|
||||
{
|
||||
_logger.RestApiRateLimitRetry(callResult.RequestId!.Value, retryTime.Value);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the parameters of the request to the request object body
|
||||
/// </summary>
|
||||
/// <param name="request">The request to set the parameters on</param>
|
||||
/// <param name="parameters">The parameters to set</param>
|
||||
/// <param name="contentType">The content type of the data</param>
|
||||
protected virtual void WriteParamBody(IRequest request, IDictionary<string, object> parameters, string contentType)
|
||||
{
|
||||
if (contentType == Constants.JsonContentHeader)
|
||||
{
|
||||
var serializer = CreateSerializer();
|
||||
if (serializer is not IStringMessageSerializer stringSerializer)
|
||||
throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
|
||||
|
||||
// Write the parameters as json in the body
|
||||
string stringData;
|
||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
stringData = stringSerializer.Serialize(value);
|
||||
else
|
||||
stringData = stringSerializer.Serialize(parameters);
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
else if (contentType == Constants.FormContentHeader)
|
||||
{
|
||||
// Write the parameters as form data in the body
|
||||
var stringData = parameters.ToFormData();
|
||||
request.SetContent(stringData, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the parameter IDictionary
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <returns></returns>
|
||||
protected internal IDictionary<string, object> CreateParameterDictionary(IDictionary<string, object> parameters)
|
||||
{
|
||||
if (!OrderParameters)
|
||||
return parameters;
|
||||
|
||||
return new SortedDictionary<string, object>(parameters, ParameterOrderComparer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||
/// </summary>
|
||||
/// <returns>Server time</returns>
|
||||
protected virtual Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void SetOptions<T>(UpdateOptions<T> options)
|
||||
{
|
||||
base.SetOptions(options);
|
||||
|
||||
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval);
|
||||
}
|
||||
|
||||
internal async ValueTask<Error?> SyncTimeAsync()
|
||||
{
|
||||
var timeSyncParams = GetTimeSyncInfo();
|
||||
if (timeSyncParams == null)
|
||||
return null;
|
||||
|
||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||
{
|
||||
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return null;
|
||||
}
|
||||
|
||||
var localTime = DateTime.UtcNow;
|
||||
var result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return result.Error;
|
||||
}
|
||||
|
||||
if (TotalRequestsMade == 1)
|
||||
{
|
||||
// If this was the first request make another one to calculate the offset since the first one can be slower
|
||||
localTime = DateTime.UtcNow;
|
||||
result = await GetServerTimestampAsync().ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
return result.Error;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate time offset between local and server
|
||||
var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2);
|
||||
timeSyncParams.UpdateTimeOffset(offset);
|
||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool ShouldCache(RequestDefinition definition)
|
||||
=> ClientOptions.CachingEnabled
|
||||
&& definition.Method == HttpMethod.Get
|
||||
&& !definition.PreventCaching;
|
||||
|
||||
}
|
||||
}
|
||||
1083
CryptoExchange.Net/Clients/SocketApiClient.cs
Normal file
1083
CryptoExchange.Net/Clients/SocketApiClient.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,200 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
||||
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
||||
/// </summary>
|
||||
public class ArrayConverter : JsonConverter
|
||||
{
|
||||
private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>();
|
||||
private static readonly ConcurrentDictionary<(Type, Type), Attribute> attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (objectType == typeof(JToken))
|
||||
return JToken.Load(reader);
|
||||
|
||||
var result = Activator.CreateInstance(objectType);
|
||||
var arr = JArray.Load(reader);
|
||||
return ParseObject(arr, result, objectType);
|
||||
}
|
||||
|
||||
private static object? ParseObject(JArray arr, object result, Type objectType)
|
||||
{
|
||||
foreach (var property in objectType.GetProperties())
|
||||
{
|
||||
var attribute = GetCustomAttribute<ArrayPropertyAttribute>(property);
|
||||
|
||||
if (attribute == null)
|
||||
continue;
|
||||
|
||||
if (attribute.Index >= arr.Count)
|
||||
continue;
|
||||
|
||||
if (property.PropertyType.BaseType == typeof(Array))
|
||||
{
|
||||
var objType = property.PropertyType.GetElementType();
|
||||
var innerArray = (JArray)arr[attribute.Index];
|
||||
var count = 0;
|
||||
if (innerArray.Count == 0)
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 });
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
else if (innerArray[0].Type == JTokenType.Array)
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count });
|
||||
foreach (var obj in innerArray)
|
||||
{
|
||||
var innerObj = Activator.CreateInstance(objType);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType);
|
||||
count++;
|
||||
}
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
|
||||
var innerObj = Activator.CreateInstance(objType);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj, objType);
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(property) ?? GetCustomAttribute<JsonConverterAttribute>(property.PropertyType);
|
||||
var conversionAttribute = GetCustomAttribute<JsonConversionAttribute>(property) ?? GetCustomAttribute<JsonConversionAttribute>(property.PropertyType);
|
||||
|
||||
object? value;
|
||||
if (converterAttribute != null)
|
||||
{
|
||||
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}});
|
||||
}
|
||||
else if (conversionAttribute != null)
|
||||
{
|
||||
value = arr[attribute.Index].ToObject(property.PropertyType);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = arr[attribute.Index];
|
||||
}
|
||||
|
||||
if (value != null && property.PropertyType.IsInstanceOfType(value))
|
||||
property.SetValue(result, value);
|
||||
else
|
||||
{
|
||||
if (value is JToken token)
|
||||
if (token.Type == JTokenType.Null)
|
||||
value = null;
|
||||
|
||||
if ((property.PropertyType == typeof(decimal)
|
||||
|| property.PropertyType == typeof(decimal?))
|
||||
&& (value != null && value.ToString().Contains("e")))
|
||||
{
|
||||
if (decimal.TryParse(value.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
property.SetValue(result, dec);
|
||||
}
|
||||
else
|
||||
{
|
||||
property.SetValue(result, value == null ? null : Convert.ChangeType(value, property.PropertyType));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
writer.WriteStartArray();
|
||||
var props = value.GetType().GetProperties();
|
||||
var ordered = props.OrderBy(p => GetCustomAttribute<ArrayPropertyAttribute>(p)?.Index);
|
||||
|
||||
var last = -1;
|
||||
foreach (var prop in ordered)
|
||||
{
|
||||
var arrayProp = GetCustomAttribute<ArrayPropertyAttribute>(prop);
|
||||
if (arrayProp == null)
|
||||
continue;
|
||||
|
||||
if (arrayProp.Index == last)
|
||||
continue;
|
||||
|
||||
while (arrayProp.Index != last + 1)
|
||||
{
|
||||
writer.WriteValue((string?)null);
|
||||
last += 1;
|
||||
}
|
||||
|
||||
last = arrayProp.Index;
|
||||
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
|
||||
if (converterAttribute != null)
|
||||
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)));
|
||||
else if (!IsSimple(prop.PropertyType))
|
||||
serializer.Serialize(writer, prop.GetValue(value));
|
||||
else
|
||||
writer.WriteValue(prop.GetValue(value));
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
|
||||
(T?)attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
|
||||
|
||||
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
||||
(T?)attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark property as an index in the array
|
||||
/// </summary>
|
||||
public class ArrayPropertyAttribute: Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The index in the array
|
||||
/// </summary>
|
||||
public int Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="index"></param>
|
||||
public ArrayPropertyAttribute(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs
Normal file
25
CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Mark property as an index in the array
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ArrayPropertyAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The index in the array
|
||||
/// </summary>
|
||||
public int Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="index"></param>
|
||||
public ArrayPropertyAttribute(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for enum converters
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of enum to convert</typeparam>
|
||||
public abstract class BaseConverter<T>: JsonConverter where T: struct
|
||||
{
|
||||
/// <summary>
|
||||
/// The enum->string mapping
|
||||
/// </summary>
|
||||
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
|
||||
private readonly bool quotes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="useQuotes"></param>
|
||||
protected BaseConverter(bool useQuotes)
|
||||
{
|
||||
quotes = useQuotes;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
var stringValue = value == null? null: GetValue((T) value);
|
||||
if (quotes)
|
||||
writer.WriteValue(stringValue);
|
||||
else
|
||||
writer.WriteRawValue(stringValue);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
if (!GetValue(reader.Value.ToString(), out var result))
|
||||
{
|
||||
Debug.WriteLine($"Cannot map enum. Type: {typeof(T)}, Value: {reader.Value}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a string value
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public T ReadString(string data)
|
||||
{
|
||||
return Mapping.FirstOrDefault(v => v.Value == data).Key;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
// Check if it is type, or nullable of type
|
||||
return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T);
|
||||
}
|
||||
|
||||
private bool GetValue(string value, out T result)
|
||||
{
|
||||
//check for exact match first, then if not found fallback to a case insensitive match
|
||||
var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||
if(mapping.Equals(default(KeyValuePair<T, string>)))
|
||||
mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
||||
{
|
||||
result = mapping.Key;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetValue(T value)
|
||||
{
|
||||
return Mapping.FirstOrDefault(v => v.Key.Equals(value)).Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
29
CryptoExchange.Net/Converters/JsonSerializerContextCache.cs
Normal file
29
CryptoExchange.Net/Converters/JsonSerializerContextCache.cs
Normal file
@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Caching for JsonSerializerContext instances
|
||||
/// </summary>
|
||||
public static class JsonSerializerContextCache
|
||||
{
|
||||
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
|
||||
|
||||
/// <summary>
|
||||
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
|
||||
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
|
||||
{
|
||||
var contextType = typeof(T);
|
||||
if (_cache.TryGetValue(contextType, out var context))
|
||||
return context;
|
||||
|
||||
var instance = new T();
|
||||
_cache[contextType] = instance;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.IO;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// REST message handler
|
||||
/// </summary>
|
||||
public interface IRestMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The `accept` HTTP response header for the request
|
||||
/// </summary>
|
||||
MediaTypeWithQualityHeaderValue AcceptHeader { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether a seekable stream is required
|
||||
/// </summary>
|
||||
bool RequiresSeekableStream { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Parse the response when the HTTP response status indicated an error
|
||||
/// </summary>
|
||||
ValueTask<Error> ParseErrorResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <summary>
|
||||
/// Parse the response when the HTTP response status indicated a rate limit error
|
||||
/// </summary>
|
||||
ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the response is an error response; if so return the error.<br />
|
||||
/// Note that if the API returns a standard result wrapper, something like this:
|
||||
/// <code>{ "code": 400, "msg": "error", "data": {} }</code>
|
||||
/// then the `CheckDeserializedResponse` method should be used for checking the result
|
||||
/// </summary>
|
||||
ValueTask<Error?> CheckForErrorResponse(
|
||||
RequestDefinition request,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize the response stream
|
||||
/// </summary>
|
||||
ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(
|
||||
Stream responseStream,
|
||||
CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the resulting T object indicates an error or not
|
||||
/// </summary>
|
||||
Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// WebSocket message handler
|
||||
/// </summary>
|
||||
public interface ISocketMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Get an identifier for the message which can be used to determine the type of the message
|
||||
/// </summary>
|
||||
string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
|
||||
|
||||
/// <summary>
|
||||
/// Get optional topic filter, for example a symbol name
|
||||
/// </summary>
|
||||
string? GetTopicFilter(object deserializedObject);
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize to the provided type
|
||||
/// </summary>
|
||||
object Deserialize(ReadOnlySpan<byte> data, Type type);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Message type definition
|
||||
/// </summary>
|
||||
public class MessageTypeDefinition
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether to immediately select the definition when it is matched. Can only be used when the evaluator has a single unique field to look for
|
||||
/// </summary>
|
||||
public bool ForceIfFound { get; set; }
|
||||
/// <summary>
|
||||
/// The fields a message needs to contain for this definition
|
||||
/// </summary>
|
||||
public MessageFieldReference[] Fields { get; set; } = [];
|
||||
/// <summary>
|
||||
/// The callback for getting the identifier string
|
||||
/// </summary>
|
||||
public Func<SearchResult, string>? TypeIdentifierCallback { get; set; }
|
||||
/// <summary>
|
||||
/// The static identifier string to return when this evaluator is matched
|
||||
/// </summary>
|
||||
public string? StaticIdentifier { get; set; }
|
||||
|
||||
internal string? GetMessageType(SearchResult result)
|
||||
{
|
||||
if (StaticIdentifier != null)
|
||||
return StaticIdentifier;
|
||||
|
||||
return TypeIdentifierCallback!(result);
|
||||
}
|
||||
|
||||
internal bool Satisfied(SearchResult result)
|
||||
{
|
||||
foreach(var field in Fields)
|
||||
{
|
||||
if (!result.Contains(field))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
internal class MessageEvalutorFieldReference
|
||||
{
|
||||
public bool SkipReading { get; set; }
|
||||
public bool OverlappingField { get; set; }
|
||||
public MessageFieldReference Field { get; set; }
|
||||
public MessageTypeDefinition? ForceEvaluator { get; set; }
|
||||
|
||||
public MessageEvalutorFieldReference(MessageFieldReference field)
|
||||
{
|
||||
Field = field;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Reference to a message field
|
||||
/// </summary>
|
||||
public abstract class MessageFieldReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The name for this search field
|
||||
/// </summary>
|
||||
public string SearchName { get; set; }
|
||||
/// <summary>
|
||||
/// The depth at which to look for this field
|
||||
/// </summary>
|
||||
public int Depth { get; set; } = 1;
|
||||
/// <summary>
|
||||
/// Callback to check if the field value matches an expected constraint
|
||||
/// </summary>
|
||||
public Func<string?, bool>? Constraint { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is one of the string values in the set
|
||||
/// </summary>
|
||||
public MessageFieldReference WithFilterConstraint(HashSet<string?> set)
|
||||
{
|
||||
Constraint = set.Contains;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is equal to a string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithEqualConstraint(string compare)
|
||||
{
|
||||
Constraint = x => x != null && x.Equals(compare, StringComparison.Ordinal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is not equal to a string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithNotEqualConstraint(string compare)
|
||||
{
|
||||
Constraint = x => x == null || !x.Equals(compare, StringComparison.Ordinal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value is not null
|
||||
/// </summary>
|
||||
public MessageFieldReference WithNotNullConstraint()
|
||||
{
|
||||
Constraint = x => x != null;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value starts with a certain string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithStartsWithConstraint(string start)
|
||||
{
|
||||
Constraint = x => x != null && x.StartsWith(start, StringComparison.Ordinal);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value starts with a certain string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithStartsWithConstraints(params string[] startValues)
|
||||
{
|
||||
Constraint = x =>
|
||||
{
|
||||
if (x == null)
|
||||
return false;
|
||||
|
||||
foreach (var item in startValues)
|
||||
{
|
||||
if (x!.StartsWith(item, StringComparison.Ordinal))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the value starts with a certain string
|
||||
/// </summary>
|
||||
public MessageFieldReference WithCustomConstraint(Func<string?, bool> constraint)
|
||||
{
|
||||
Constraint = constraint;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public MessageFieldReference(string searchName)
|
||||
{
|
||||
SearchName = searchName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to a property message field
|
||||
/// </summary>
|
||||
public class PropertyFieldReference : MessageFieldReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The property name in the JSON
|
||||
/// </summary>
|
||||
public byte[] PropertyName { get; set; }
|
||||
/// <summary>
|
||||
/// Whether the property value is array values
|
||||
/// </summary>
|
||||
public bool ArrayValues { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public PropertyFieldReference(string propertyName) : base(propertyName)
|
||||
{
|
||||
PropertyName = Encoding.UTF8.GetBytes(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to an array message field
|
||||
/// </summary>
|
||||
public class ArrayFieldReference : MessageFieldReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The index in the array
|
||||
/// </summary>
|
||||
public int ArrayIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
|
||||
{
|
||||
Depth = depth;
|
||||
ArrayIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// The results of a search for fields in a JSON message
|
||||
/// </summary>
|
||||
public class SearchResult
|
||||
{
|
||||
private List<SearchResultItem> _items = new List<SearchResultItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Get the value of a field
|
||||
/// </summary>
|
||||
public string? FieldValue(string searchName)
|
||||
{
|
||||
foreach (var item in _items)
|
||||
{
|
||||
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
|
||||
return item.Value;
|
||||
}
|
||||
|
||||
throw new Exception($"No field value found for {searchName}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of found search field values
|
||||
/// </summary>
|
||||
public int Count => _items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Clear the search result
|
||||
/// </summary>
|
||||
public void Clear() => _items.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the value for a specific field was found
|
||||
/// </summary>
|
||||
public bool Contains(MessageFieldReference field)
|
||||
{
|
||||
foreach (var item in _items)
|
||||
{
|
||||
if (item.Field == field)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a value to the result
|
||||
/// </summary>
|
||||
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
|
||||
{
|
||||
Field = field,
|
||||
Value = value
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Search result value
|
||||
/// </summary>
|
||||
public struct SearchResultItem
|
||||
{
|
||||
/// <summary>
|
||||
/// The field the values is for
|
||||
/// </summary>
|
||||
public MessageFieldReference Field { get; set; }
|
||||
/// <summary>
|
||||
/// The value of the field
|
||||
/// </summary>
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
}
|
||||
49
CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs
Normal file
49
CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs
Normal file
@ -0,0 +1,49 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Node accessor
|
||||
/// </summary>
|
||||
public readonly struct NodeAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Index
|
||||
/// </summary>
|
||||
public int? Index { get; }
|
||||
/// <summary>
|
||||
/// Property name
|
||||
/// </summary>
|
||||
public string? Property { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Type (0 = int, 1 = string, 2 = prop name)
|
||||
/// </summary>
|
||||
public int Type { get; }
|
||||
|
||||
private NodeAccessor(int? index, string? property, int type)
|
||||
{
|
||||
Index = index;
|
||||
Property = property;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an int node accessor
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
|
||||
|
||||
/// <summary>
|
||||
/// Create a string node accessor
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
|
||||
|
||||
/// <summary>
|
||||
/// Create a property name node accessor
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
|
||||
}
|
||||
}
|
||||
50
CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs
Normal file
50
CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message access definition
|
||||
/// </summary>
|
||||
public readonly struct MessagePath : IEnumerable<NodeAccessor>
|
||||
{
|
||||
private readonly List<NodeAccessor> _path;
|
||||
|
||||
internal void Add(NodeAccessor node)
|
||||
{
|
||||
_path.Add(node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public MessagePath()
|
||||
{
|
||||
_path = new List<NodeAccessor>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new message path
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static MessagePath Get()
|
||||
{
|
||||
return new MessagePath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IEnumerable implementation
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerator<NodeAccessor> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < _path.Count; i++)
|
||||
yield return _path[i];
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message path extension methods
|
||||
/// </summary>
|
||||
public static class MessagePathExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a string node accessor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="propName"></param>
|
||||
/// <returns></returns>
|
||||
public static MessagePath Property(this MessagePath path, string propName)
|
||||
{
|
||||
path.Add(NodeAccessor.String(propName));
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a property name node accessor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static MessagePath PropertyName(this MessagePath path)
|
||||
{
|
||||
path.Add(NodeAccessor.PropertyName());
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a int node accessor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public static MessagePath Index(this MessagePath path, int index)
|
||||
{
|
||||
path.Add(NodeAccessor.Int(index));
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
CryptoExchange.Net/Converters/MessageParsing/NodeType.cs
Normal file
21
CryptoExchange.Net/Converters/MessageParsing/NodeType.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message node type
|
||||
/// </summary>
|
||||
public enum NodeType
|
||||
{
|
||||
/// <summary>
|
||||
/// Array node
|
||||
/// </summary>
|
||||
Array,
|
||||
/// <summary>
|
||||
/// Object node
|
||||
/// </summary>
|
||||
Object,
|
||||
/// <summary>
|
||||
/// Value node
|
||||
/// </summary>
|
||||
Value
|
||||
}
|
||||
}
|
||||
246
CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs
Normal file
246
CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs
Normal file
@ -0,0 +1,246 @@
|
||||
using CryptoExchange.Net.Exceptions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
||||
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
|
||||
#else
|
||||
public class ArrayConverter<T> : JsonConverter<T> where T : new()
|
||||
#endif
|
||||
{
|
||||
private static SortedDictionary<int, List<ArrayPropertyInfo>>? _typePropertyInfo;
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_typePropertyInfo == null)
|
||||
_typePropertyInfo = CacheTypeAttributes();
|
||||
|
||||
writer.WriteStartArray();
|
||||
var last = -1;
|
||||
foreach (var indexProps in _typePropertyInfo)
|
||||
{
|
||||
foreach (var prop in indexProps.Value)
|
||||
{
|
||||
if (prop.ArrayProperty.Index == last)
|
||||
// Don't write the same index twice
|
||||
continue;
|
||||
|
||||
while (prop.ArrayProperty.Index != last + 1)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
last += 1;
|
||||
}
|
||||
|
||||
last = prop.ArrayProperty.Index;
|
||||
|
||||
var objValue = prop.PropertyInfo.GetValue(value);
|
||||
if (objValue == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonSerializerOptions? typeOptions = null;
|
||||
if (prop.JsonConverter != null)
|
||||
{
|
||||
typeOptions = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
TypeInfoResolver = options.TypeInfoResolver,
|
||||
};
|
||||
typeOptions.Converters.Add(prop.JsonConverter);
|
||||
}
|
||||
|
||||
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
|
||||
{
|
||||
if (prop.TargetType == typeof(string))
|
||||
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
||||
else if (prop.TargetType == typeof(bool))
|
||||
writer.WriteBooleanValue((bool)objValue);
|
||||
else
|
||||
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return default;
|
||||
|
||||
var result = new T();
|
||||
return ParseObject(ref reader, result, options);
|
||||
}
|
||||
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||
#else
|
||||
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||
#endif
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new CeDeserializationException("Not an array");
|
||||
|
||||
|
||||
if (_typePropertyInfo == null)
|
||||
_typePropertyInfo = CacheTypeAttributes();
|
||||
|
||||
int index = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndArray)
|
||||
break;
|
||||
|
||||
if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes))
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var attribute in indexAttributes)
|
||||
{
|
||||
var targetType = attribute.TargetType;
|
||||
object? value = null;
|
||||
if (attribute.JsonConverter != null)
|
||||
{
|
||||
if (attribute.JsonSerializerOptions == null)
|
||||
{
|
||||
attribute.JsonSerializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
Converters = { attribute.JsonConverter },
|
||||
TypeInfoResolver = options.TypeInfoResolver,
|
||||
};
|
||||
}
|
||||
|
||||
var doc = JsonDocument.ParseValue(ref reader);
|
||||
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
|
||||
}
|
||||
else if (attribute.DefaultDeserialization)
|
||||
{
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
|
||||
}
|
||||
else
|
||||
{
|
||||
value = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.Null => null,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetDecimal(),
|
||||
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
|
||||
_ => throw new CeDeserializationException($"Array deserialization of type {reader.TokenType} not supported"),
|
||||
};
|
||||
}
|
||||
|
||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
||||
attribute.PropertyInfo.SetValue(result, value);
|
||||
else
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
|
||||
#else
|
||||
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
|
||||
#endif
|
||||
{
|
||||
var result = new SortedDictionary<int, List<ArrayPropertyInfo>>();
|
||||
var properties = typeof(T).GetProperties();
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
||||
if (att == null)
|
||||
continue;
|
||||
|
||||
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
|
||||
if (!result.TryGetValue(att.Index, out var indexList))
|
||||
{
|
||||
indexList = new List<ArrayPropertyInfo>();
|
||||
result[att.Index] = indexList;
|
||||
}
|
||||
|
||||
indexList.Add(new ArrayPropertyInfo
|
||||
{
|
||||
ArrayProperty = att,
|
||||
PropertyInfo = property,
|
||||
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
|
||||
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
|
||||
TargetType = targetType
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private class ArrayPropertyInfo
|
||||
{
|
||||
public PropertyInfo PropertyInfo { get; set; } = null!;
|
||||
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
||||
public JsonConverter? JsonConverter { get; set; }
|
||||
public bool DefaultDeserialization { get; set; }
|
||||
public Type TargetType { get; set; } = null!;
|
||||
public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
||||
/// </summary>
|
||||
public class BigDecimalConverter : JsonConverter<decimal>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
try
|
||||
{
|
||||
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch(OverflowException)
|
||||
{
|
||||
// Value doesn't fit decimal, default to max value
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return reader.GetDecimal();
|
||||
}
|
||||
catch(FormatException)
|
||||
{
|
||||
// Format issue, assume value is too large
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteNumberValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Bool converter
|
||||
/// </summary>
|
||||
public class BoolConverter : JsonConverterFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInnerNullable();
|
||||
}
|
||||
|
||||
private class BoolConverterInnerNullable : JsonConverter<bool?>
|
||||
{
|
||||
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ReadBool(ref reader, typeToConvert, options);
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is bool boolVal)
|
||||
writer.WriteBooleanValue(boolVal);
|
||||
else
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
|
||||
private class BoolConverterInner : JsonConverter<bool>
|
||||
{
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ReadBool(ref reader, typeToConvert, options) ?? false;
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteBooleanValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.True)
|
||||
return true;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.False)
|
||||
return false;
|
||||
|
||||
var value = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
value = value?.ToLowerInvariant().Trim();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
if (typeToConvert == typeof(bool))
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case "true":
|
||||
case "yes":
|
||||
case "y":
|
||||
case "1":
|
||||
case "on":
|
||||
return true;
|
||||
case "false":
|
||||
case "no":
|
||||
case "n":
|
||||
case "0":
|
||||
case "off":
|
||||
case "-1":
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new SerializationException($"Can't convert bool value {value}");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for comma separated enum values
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
|
||||
#else
|
||||
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
|
||||
#endif
|
||||
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var str = reader.GetString();
|
||||
if (string.IsNullOrEmpty(str))
|
||||
return [];
|
||||
|
||||
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,273 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Date time converter
|
||||
/// </summary>
|
||||
public class DateTimeConverter : JsonConverterFactory
|
||||
{
|
||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
|
||||
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
|
||||
}
|
||||
|
||||
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
|
||||
{
|
||||
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ReadDateTime(ref reader, typeToConvert, options);
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.Value == default)
|
||||
writer.WriteStringValue(default(DateTime));
|
||||
else
|
||||
writer.WriteNumberValue((long)Math.Round((value.Value - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
|
||||
private class DateTimeConverterInner : JsonConverter<DateTime>
|
||||
{
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> ReadDateTime(ref reader, typeToConvert, options) ?? default;
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
{
|
||||
var dtValue = value;
|
||||
if (dtValue == default)
|
||||
writer.WriteStringValue(default(DateTime));
|
||||
else
|
||||
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
|
||||
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
if (typeToConvert == typeof(DateTime))
|
||||
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is JsonTokenType.Number)
|
||||
{
|
||||
var decValue = reader.GetDecimal();
|
||||
if (decValue == 0 || decValue < 0)
|
||||
return default;
|
||||
|
||||
return ParseFromDecimal(decValue);
|
||||
}
|
||||
else if (reader.TokenType is JsonTokenType.String)
|
||||
{
|
||||
var stringValue = reader.GetString();
|
||||
if (string.IsNullOrWhiteSpace(stringValue)
|
||||
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|
||||
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|
||||
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
return reader.GetDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a double value to datetime
|
||||
/// </summary>
|
||||
public static DateTime ParseFromDouble(double value)
|
||||
=> ParseFromDecimal((decimal)value);
|
||||
|
||||
/// <summary>
|
||||
/// Parse a decimal value to datetime
|
||||
/// </summary>
|
||||
public static DateTime ParseFromDecimal(decimal value)
|
||||
{
|
||||
if (value < 19999999999)
|
||||
return ConvertFromSeconds(value);
|
||||
if (value < 19999999999999)
|
||||
return ConvertFromMilliseconds(value);
|
||||
if (value < 19999999999999999)
|
||||
return ConvertFromMicroseconds(value);
|
||||
|
||||
return ConvertFromNanoseconds(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a string value to datetime
|
||||
/// </summary>
|
||||
public static DateTime ParseFromString(string stringValue, string? resolverName)
|
||||
{
|
||||
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Parse 202303261200 format
|
||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||
{
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (stringValue.Length == 8)
|
||||
{
|
||||
// Parse 20211103 format
|
||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||
{
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (stringValue.Length == 6)
|
||||
{
|
||||
// Parse 211103 format
|
||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||
{
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
|
||||
{
|
||||
// Parse 1637745563.000 format
|
||||
if (decimalValue <= 0)
|
||||
return default;
|
||||
if (decimalValue < 19999999999)
|
||||
return ConvertFromSeconds(decimalValue);
|
||||
if (decimalValue < 19999999999999)
|
||||
return ConvertFromMilliseconds(decimalValue);
|
||||
if (decimalValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds(decimalValue);
|
||||
|
||||
return ConvertFromNanoseconds(decimalValue);
|
||||
}
|
||||
|
||||
if (stringValue.Length == 10)
|
||||
{
|
||||
// Parse 2021-11-03 format
|
||||
var values = stringValue.Split('-');
|
||||
if (!int.TryParse(values[0], out var year)
|
||||
|| !int.TryParse(values[1], out var month)
|
||||
|| !int.TryParse(values[2], out var day))
|
||||
{
|
||||
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||
return default;
|
||||
}
|
||||
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
|
||||
/// <summary>
|
||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||
/// <summary>
|
||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Decimal converter
|
||||
/// </summary>
|
||||
public class DecimalConverter : JsonConverter<decimal?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return null;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
return ExchangeHelpers.ParseDecimal(value);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return reader.GetDecimal();
|
||||
}
|
||||
catch(FormatException)
|
||||
{
|
||||
// Format issue, assume value is too large
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for serializing decimal values as string
|
||||
/// </summary>
|
||||
public class DecimalStringWriterConverter : JsonConverter<decimal>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
|
||||
}
|
||||
}
|
||||
359
CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs
Normal file
359
CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs
Normal file
@ -0,0 +1,359 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
#if NET8_0_OR_GREATER
|
||||
using System.Collections.Frozen;
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Static EnumConverter methods
|
||||
/// </summary>
|
||||
public static class EnumConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the enum value from a string
|
||||
/// </summary>
|
||||
/// <param name="value">String value</param>
|
||||
/// <returns></returns>
|
||||
#if NET5_0_OR_GREATER
|
||||
public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
|
||||
#else
|
||||
public static T? ParseString<T>(string value) where T : struct, Enum
|
||||
#endif
|
||||
=> EnumConverter<T>.ParseString(value);
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||
/// </summary>
|
||||
/// <param name="enumValue"></param>
|
||||
/// <returns></returns>
|
||||
#if NET5_0_OR_GREATER
|
||||
public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum
|
||||
#else
|
||||
public static string GetString<T>(T enumValue) where T : struct, Enum
|
||||
#endif
|
||||
=> EnumConverter<T>.GetString(enumValue);
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||
/// </summary>
|
||||
/// <param name="enumValue"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
#if NET5_0_OR_GREATER
|
||||
public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum
|
||||
#else
|
||||
public static string? GetString<T>(T? enumValue) where T : struct, Enum
|
||||
#endif
|
||||
=> EnumConverter<T>.GetString(enumValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||
/// </summary>
|
||||
#if NET5_0_OR_GREATER
|
||||
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
|
||||
#else
|
||||
public class EnumConverter<T>
|
||||
#endif
|
||||
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
|
||||
{
|
||||
class EnumMapping
|
||||
{
|
||||
public T Value { get; set; }
|
||||
public string StringValue { get; set; }
|
||||
|
||||
public EnumMapping(T value, string stringValue)
|
||||
{
|
||||
Value = value;
|
||||
StringValue = stringValue;
|
||||
}
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
|
||||
private static FrozenDictionary<T, string>? _mappingToString = null;
|
||||
#else
|
||||
private static List<EnumMapping>? _mappingToEnum = null;
|
||||
private static Dictionary<T, string>? _mappingToString = null;
|
||||
#endif
|
||||
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||
|
||||
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||
|
||||
internal class NullableEnumConverter : JsonConverter<T?>
|
||||
{
|
||||
private readonly EnumConverter<T> _enumConverter;
|
||||
|
||||
public NullableEnumConverter(EnumConverter<T> enumConverter)
|
||||
{
|
||||
_enumConverter = enumConverter;
|
||||
}
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
_enumConverter.Write(writer, value.Value, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||
if (t == null)
|
||||
{
|
||||
if (isEmptyString && !_unknownValuesWarned.Contains(null))
|
||||
{
|
||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
|
||||
return new T(); // return default value
|
||||
}
|
||||
else
|
||||
{
|
||||
return t.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
|
||||
{
|
||||
isEmptyString = false;
|
||||
var enumType = typeof(T);
|
||||
if (_mappingToEnum == null)
|
||||
CreateMapping();
|
||||
|
||||
var stringValue = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetInt32().ToString(),
|
||||
JsonTokenType.True => reader.GetBoolean().ToString(),
|
||||
JsonTokenType.False => reader.GetBoolean().ToString(),
|
||||
JsonTokenType.Null => null,
|
||||
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
||||
};
|
||||
|
||||
if (stringValue is null)
|
||||
return null;
|
||||
|
||||
if (!GetValue(enumType, stringValue, out var result))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
{
|
||||
isEmptyString = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We received an enum value but weren't able to parse it.
|
||||
if (!_unknownValuesWarned.Contains(stringValue))
|
||||
{
|
||||
_unknownValuesWarned.Add(stringValue!);
|
||||
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: {string.Join(", ", _mappingToEnum!.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
var stringValue = GetString(value);
|
||||
writer.WriteStringValue(stringValue);
|
||||
}
|
||||
|
||||
private static bool GetValue(Type objectType, string value, out T? result)
|
||||
{
|
||||
if (_mappingToEnum != null)
|
||||
{
|
||||
EnumMapping? mapping = null;
|
||||
// Try match on full equals
|
||||
foreach (var item in _mappingToEnum)
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try matching ignoring case
|
||||
if (mapping == null)
|
||||
{
|
||||
foreach (var item in _mappingToEnum)
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping != null)
|
||||
{
|
||||
result = mapping.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
||||
{
|
||||
var intValue = int.Parse(value);
|
||||
result = (T)Enum.ToObject(objectType, intValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_unknownValuesWarned.Contains(value))
|
||||
{
|
||||
// Check if it is an known unknown value
|
||||
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(value))
|
||||
{
|
||||
// An empty/null value will always fail when parsing, so just return here
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
result = (T)Enum.Parse(objectType, value, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateMapping()
|
||||
{
|
||||
var mappingToEnum = new List<EnumMapping>();
|
||||
var mappingToString = new Dictionary<T, string>();
|
||||
|
||||
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
var enumMembers = enumType.GetFields();
|
||||
foreach (var member in enumMembers)
|
||||
{
|
||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
||||
foreach (MapAttribute attribute in maps)
|
||||
{
|
||||
foreach (var value in attribute.Values)
|
||||
{
|
||||
var enumVal = (T)Enum.Parse(enumType, member.Name);
|
||||
mappingToEnum.Add(new EnumMapping(enumVal, value));
|
||||
if (!mappingToString.ContainsKey(enumVal))
|
||||
mappingToString.Add(enumVal, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if NET8_0_OR_GREATER
|
||||
_mappingToEnum = mappingToEnum.ToFrozenSet();
|
||||
_mappingToString = mappingToString.ToFrozenDictionary();
|
||||
#else
|
||||
_mappingToEnum = mappingToEnum;
|
||||
_mappingToString = mappingToString;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||
/// </summary>
|
||||
/// <param name="enumValue"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
public static string? GetString(T? enumValue)
|
||||
{
|
||||
if (_mappingToString == null)
|
||||
CreateMapping();
|
||||
|
||||
return enumValue == null ? null : (_mappingToString!.TryGetValue(enumValue.Value, out var str) ? str : enumValue.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the enum value from a string
|
||||
/// </summary>
|
||||
/// <param name="value">String value</param>
|
||||
/// <returns></returns>
|
||||
public static T? ParseString(string value)
|
||||
{
|
||||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||
if (_mappingToEnum == null)
|
||||
CreateMapping();
|
||||
|
||||
EnumMapping? mapping = null;
|
||||
// Try match on full equals
|
||||
foreach(var item in _mappingToEnum!)
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found, try matching ignoring case
|
||||
if (mapping == null)
|
||||
{
|
||||
foreach (var item in _mappingToEnum)
|
||||
{
|
||||
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mapping = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping != null)
|
||||
return mapping.Value;
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
return (T)Enum.Parse(type, value, true);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public JsonConverter CreateNullableConverter()
|
||||
{
|
||||
_nullableEnumConverter ??= new NullableEnumConverter(this);
|
||||
return _nullableEnumConverter;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for serializing enum values as int
|
||||
/// </summary>
|
||||
public class EnumIntWriterConverter<T> : JsonConverter<T> where T: struct, Enum
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
=> writer.WriteNumberValue((int)(object)value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
internal interface INullableConverterFactory
|
||||
{
|
||||
JsonConverter CreateNullableConverter();
|
||||
}
|
||||
}
|
||||
40
CryptoExchange.Net/Converters/SystemTextJson/IntConverter.cs
Normal file
40
CryptoExchange.Net/Converters/SystemTextJson/IntConverter.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Int converter
|
||||
/// </summary>
|
||||
public class IntConverter : JsonConverter<int?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return null;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return null;
|
||||
|
||||
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return reader.GetInt32();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Int converter
|
||||
/// </summary>
|
||||
public class LongConverter : JsonConverter<long?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return null;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return null;
|
||||
|
||||
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return reader.GetInt64();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Errors;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON REST message handler
|
||||
/// </summary>
|
||||
public abstract class JsonRestMessageHandler : IRestMessageHandler
|
||||
{
|
||||
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
|
||||
|
||||
/// <summary>
|
||||
/// Empty rate limit error
|
||||
/// </summary>
|
||||
protected static readonly ServerRateLimitError _emptyRateLimitError = new ServerRateLimitError();
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool RequiresSeekableStream => false;
|
||||
|
||||
/// <summary>
|
||||
/// The serializer options to use
|
||||
/// </summary>
|
||||
public abstract JsonSerializerOptions Options { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public MediaTypeWithQualityHeaderValue AcceptHeader => _acceptJsonContent;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream)
|
||||
{
|
||||
// Handle retry after header
|
||||
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||
if (retryAfterHeader.Value?.Any() != true)
|
||||
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
|
||||
|
||||
var value = retryAfterHeader.Value.First();
|
||||
if (int.TryParse(value, out var seconds))
|
||||
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) });
|
||||
|
||||
if (DateTime.TryParse(value, out var datetime))
|
||||
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = datetime });
|
||||
|
||||
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract ValueTask<Error> ParseErrorResponse(
|
||||
int httpStatusCode,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual ValueTask<Error?> CheckForErrorResponse(
|
||||
RequestDefinition request,
|
||||
HttpResponseHeaders responseHeaders,
|
||||
Stream responseStream) => new ValueTask<Error?>((Error?)null);
|
||||
|
||||
/// <summary>
|
||||
/// Read the response into a JsonDocument object
|
||||
/// </summary>
|
||||
protected virtual async ValueTask<(Error?, JsonDocument?)> GetJsonDocument(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
|
||||
return (null, document);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (new ServerError(new ErrorInfo(ErrorType.DeserializationFailed, false, "Deserialization failed, invalid JSON"), ex), null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public async ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(Stream responseStream, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
var result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
return (result, null);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||
return (default, new DeserializeError(info, ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (default, new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result) => null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,348 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON WebSocket message handler, sequentially read the JSON and looks for specific predefined fields to identify the message
|
||||
/// </summary>
|
||||
public abstract class JsonSocketMessageHandler : ISocketMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The serializer options to use
|
||||
/// </summary>
|
||||
public abstract JsonSerializerOptions Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Message evaluators
|
||||
/// </summary>
|
||||
protected abstract MessageTypeDefinition[] TypeEvaluators { get; }
|
||||
|
||||
private readonly SearchResult _searchResult = new();
|
||||
|
||||
private bool _hasArraySearches;
|
||||
private bool _initialized;
|
||||
private int _maxSearchDepth;
|
||||
private MessageTypeDefinition? _topEvaluator;
|
||||
private List<MessageEvalutorFieldReference>? _searchFields;
|
||||
private Dictionary<Type, Func<object, string?>>? _baseTypeMapping;
|
||||
private Dictionary<Type, Func<object, string?>>? _mapping;
|
||||
|
||||
/// <summary>
|
||||
/// Add a mapping of a specific object of a type to a specific topic
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type to get topic for</typeparam>
|
||||
/// <param name="mapping">The topic retrieve delegate</param>
|
||||
protected void AddTopicMapping<T>(Func<T, string?> mapping)
|
||||
{
|
||||
_mapping ??= new Dictionary<Type, Func<object, string?>>();
|
||||
_mapping.Add(typeof(T), x => mapping((T)x));
|
||||
}
|
||||
|
||||
private void InitializeConverter()
|
||||
{
|
||||
if (_initialized)
|
||||
return;
|
||||
|
||||
_maxSearchDepth = int.MinValue;
|
||||
_searchFields = new List<MessageEvalutorFieldReference>();
|
||||
foreach (var evaluator in TypeEvaluators)
|
||||
{
|
||||
_topEvaluator ??= evaluator;
|
||||
foreach (var field in evaluator.Fields)
|
||||
{
|
||||
var overlapping = _searchFields.Where(otherField =>
|
||||
{
|
||||
if (field is PropertyFieldReference propRef
|
||||
&& otherField.Field is PropertyFieldReference otherPropRef)
|
||||
{
|
||||
return field.Depth == otherPropRef.Depth && propRef.PropertyName.SequenceEqual(otherPropRef.PropertyName);
|
||||
}
|
||||
else if (field is ArrayFieldReference arrayRef
|
||||
&& otherField.Field is ArrayFieldReference otherArrayPropRef)
|
||||
{
|
||||
return field.Depth == otherArrayPropRef.Depth && arrayRef.ArrayIndex == otherArrayPropRef.ArrayIndex;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).ToList();
|
||||
|
||||
if (overlapping.Any())
|
||||
{
|
||||
foreach (var overlap in overlapping)
|
||||
overlap.OverlappingField = true;
|
||||
}
|
||||
|
||||
List<MessageEvalutorFieldReference>? existingSameSearchField = new();
|
||||
if (field is ArrayFieldReference arrayField)
|
||||
{
|
||||
_hasArraySearches = true;
|
||||
existingSameSearchField = _searchFields.Where(x =>
|
||||
x.Field is ArrayFieldReference arrayFieldRef
|
||||
&& arrayFieldRef.ArrayIndex == arrayField.ArrayIndex
|
||||
&& arrayFieldRef.Depth == arrayField.Depth
|
||||
&& arrayFieldRef.Constraint == null && arrayField.Constraint == null).ToList();
|
||||
}
|
||||
else if (field is PropertyFieldReference propField)
|
||||
{
|
||||
existingSameSearchField = _searchFields.Where(x =>
|
||||
x.Field is PropertyFieldReference propFieldRef
|
||||
&& propFieldRef.PropertyName.SequenceEqual(propField.PropertyName)
|
||||
&& propFieldRef.Depth == propField.Depth
|
||||
&& propFieldRef.Constraint == null && propFieldRef.Constraint == null).ToList();
|
||||
}
|
||||
|
||||
foreach(var sameSearchField in existingSameSearchField)
|
||||
{
|
||||
if (sameSearchField.SkipReading == true
|
||||
&& (evaluator.TypeIdentifierCallback != null || field.Constraint != null))
|
||||
{
|
||||
sameSearchField.SkipReading = false;
|
||||
}
|
||||
|
||||
if (evaluator.ForceIfFound)
|
||||
{
|
||||
if (evaluator.Fields.Length > 1 || sameSearchField.ForceEvaluator != null)
|
||||
throw new Exception("Invalid config");
|
||||
|
||||
//sameSearchField.ForceEvaluator = evaluator;
|
||||
}
|
||||
}
|
||||
|
||||
_searchFields.Add(new MessageEvalutorFieldReference(field)
|
||||
{
|
||||
SkipReading = evaluator.TypeIdentifierCallback == null && field.Constraint == null,
|
||||
ForceEvaluator = !existingSameSearchField.Any() ? evaluator.ForceIfFound ? evaluator : null : null,
|
||||
OverlappingField = overlapping.Any()
|
||||
});
|
||||
|
||||
if (field.Depth > _maxSearchDepth)
|
||||
_maxSearchDepth = field.Depth;
|
||||
}
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? GetTopicFilter(object deserializedObject)
|
||||
{
|
||||
if (_mapping == null)
|
||||
return null;
|
||||
|
||||
// Cache the found type for future
|
||||
var currentType = deserializedObject.GetType();
|
||||
if (_baseTypeMapping != null)
|
||||
{
|
||||
if (_baseTypeMapping.TryGetValue(currentType, out var typeMapping))
|
||||
return typeMapping(deserializedObject);
|
||||
}
|
||||
|
||||
var mappedBase = false;
|
||||
while (currentType != null)
|
||||
{
|
||||
if (_mapping.TryGetValue(currentType, out var mapping))
|
||||
{
|
||||
if (mappedBase)
|
||||
{
|
||||
_baseTypeMapping ??= new Dictionary<Type, Func<object, string?>>();
|
||||
_baseTypeMapping.Add(deserializedObject.GetType(), mapping);
|
||||
}
|
||||
|
||||
return mapping(deserializedObject);
|
||||
}
|
||||
|
||||
mappedBase = true;
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||
{
|
||||
InitializeConverter();
|
||||
|
||||
int? arrayIndex = null;
|
||||
|
||||
_searchResult.Clear();
|
||||
var reader = new Utf8JsonReader(data);
|
||||
while (reader.Read())
|
||||
{
|
||||
if ((reader.TokenType == JsonTokenType.StartArray
|
||||
|| reader.TokenType == JsonTokenType.StartObject)
|
||||
&& reader.CurrentDepth == _maxSearchDepth)
|
||||
{
|
||||
// There is no field we need to search for on a depth deeper than this, skip
|
||||
reader.Skip();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (reader.TokenType == JsonTokenType.StartArray)
|
||||
arrayIndex = -1;
|
||||
else if (reader.TokenType == JsonTokenType.EndArray)
|
||||
arrayIndex = null;
|
||||
else if (arrayIndex != null)
|
||||
arrayIndex++;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.PropertyName
|
||||
|| arrayIndex != null && _hasArraySearches)
|
||||
{
|
||||
bool written = false;
|
||||
|
||||
string? value = null;
|
||||
byte[]? propName = null;
|
||||
foreach (var field in _searchFields!)
|
||||
{
|
||||
if (field.Field.Depth != reader.CurrentDepth)
|
||||
continue;
|
||||
|
||||
bool readArrayValues = false;
|
||||
if (field.Field is PropertyFieldReference propFieldRef)
|
||||
{
|
||||
if (propName == null)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
continue;
|
||||
|
||||
if (!reader.ValueTextEquals(propFieldRef.PropertyName))
|
||||
continue;
|
||||
|
||||
propName = propFieldRef.PropertyName;
|
||||
readArrayValues = propFieldRef.ArrayValues;
|
||||
reader.Read();
|
||||
}
|
||||
else if (!propFieldRef.PropertyName.SequenceEqual(propName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (field.Field is ArrayFieldReference arrayFieldRef)
|
||||
{
|
||||
if (propName != null)
|
||||
continue;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||
continue;
|
||||
|
||||
if (arrayFieldRef.ArrayIndex != arrayIndex)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!field.SkipReading)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
if (readArrayValues)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
// error
|
||||
return null;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
reader.Read();// Read start array
|
||||
bool first = true;
|
||||
while(reader.TokenType != JsonTokenType.EndArray)
|
||||
{
|
||||
if (!first)
|
||||
sb.Append(",");
|
||||
|
||||
first = false;
|
||||
sb.Append(reader.GetString());
|
||||
reader.Read();
|
||||
}
|
||||
|
||||
value = first ? null : sb.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.Number:
|
||||
value = reader.GetDecimal().ToString();
|
||||
break;
|
||||
case JsonTokenType.String:
|
||||
value = reader.GetString()!;
|
||||
break;
|
||||
case JsonTokenType.True:
|
||||
case JsonTokenType.False:
|
||||
value = reader.GetBoolean().ToString()!;
|
||||
break;
|
||||
case JsonTokenType.Null:
|
||||
value = null;
|
||||
break;
|
||||
case JsonTokenType.StartObject:
|
||||
case JsonTokenType.StartArray:
|
||||
value = null;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (field.Field.Constraint != null
|
||||
&& !field.Field.Constraint(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_searchResult.Write(field.Field, value);
|
||||
|
||||
if (field.ForceEvaluator != null)
|
||||
{
|
||||
if (field.ForceEvaluator.StaticIdentifier != null)
|
||||
return field.ForceEvaluator.StaticIdentifier;
|
||||
|
||||
// Force the immediate return upon encountering this field
|
||||
return field.ForceEvaluator.GetMessageType(_searchResult);
|
||||
}
|
||||
|
||||
written = true;
|
||||
if (!field.OverlappingField)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!written)
|
||||
continue;
|
||||
|
||||
if (_topEvaluator!.Satisfied(_searchResult))
|
||||
return _topEvaluator.GetMessageType(_searchResult);
|
||||
|
||||
if (_searchFields.Count == _searchResult.Count)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var evaluator in TypeEvaluators)
|
||||
{
|
||||
if (evaluator.Satisfied(_searchResult))
|
||||
return evaluator.GetMessageType(_searchResult);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
return JsonSerializer.Deserialize(data, type, Options)!;
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// JSON WebSocket message handler, reads the json data info a JsonDocument after which the data can be inspected to identify the message
|
||||
/// </summary>
|
||||
public abstract class JsonSocketPreloadMessageHandler : ISocketMessageHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// The serializer options to use
|
||||
/// </summary>
|
||||
public abstract JsonSerializerOptions Options { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||
{
|
||||
var reader = new Utf8JsonReader(data);
|
||||
var jsonDocument = JsonDocument.ParseValue(ref reader);
|
||||
|
||||
return GetTypeIdentifier(jsonDocument);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the message identifier for this document
|
||||
/// </summary>
|
||||
protected abstract string? GetTypeIdentifier(JsonDocument document);
|
||||
|
||||
/// <summary>
|
||||
/// Get optional topic filter, for example a symbol name
|
||||
/// </summary>
|
||||
public virtual string? GetTopicFilter(object deserializedObject) => null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
return JsonSerializer.Deserialize(data, type, Options)!;
|
||||
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for a path, or an emtpy string if not found
|
||||
/// </summary>
|
||||
protected string StringOrEmpty(JsonDocument document, string path)
|
||||
{
|
||||
if (!document.RootElement.TryGetProperty(path, out var element))
|
||||
return string.Empty;
|
||||
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
return element.GetString() ?? string.Empty;
|
||||
else if (element.ValueKind == JsonValueKind.Number)
|
||||
return element.GetDecimal().ToString();
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
internal class NullableEnumConverterFactory : JsonConverterFactory
|
||||
{
|
||||
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
|
||||
private static readonly JsonSerializerOptions _options = new JsonSerializerOptions();
|
||||
|
||||
public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver)
|
||||
{
|
||||
_jsonTypeInfoResolver = jsonTypeInfoResolver;
|
||||
}
|
||||
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
var b = Nullable.GetUnderlyingType(typeToConvert);
|
||||
if (b == null)
|
||||
return false;
|
||||
|
||||
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options);
|
||||
if (typeInfo == null)
|
||||
return false;
|
||||
|
||||
return typeInfo.Converter is INullableConverterFactory;
|
||||
}
|
||||
|
||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}");
|
||||
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}");
|
||||
if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory)
|
||||
throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}");
|
||||
|
||||
return nullConverterFactory.CreateNullableConverter();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Read string or number as string
|
||||
/// </summary>
|
||||
public class NumberStringConverter : JsonConverter<string?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return null;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.Number)
|
||||
{
|
||||
if (reader.TryGetInt64(out var value))
|
||||
return value.ToString();
|
||||
|
||||
return reader.GetDecimal().ToString();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return reader.GetString();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for values which contain a nested json value
|
||||
/// </summary>
|
||||
public class ObjectStringConverter<T> : JsonConverter<T>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return default;
|
||||
|
||||
var value = reader.GetString();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return default;
|
||||
|
||||
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is null)
|
||||
writer.WriteStringValue("");
|
||||
|
||||
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Replace a value on a string property
|
||||
/// </summary>
|
||||
public abstract class ReplaceConverter : JsonConverter<string>
|
||||
{
|
||||
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ReplaceConverter(params string[] replaceSets)
|
||||
{
|
||||
_replacementSets = replaceSets.Select(x =>
|
||||
{
|
||||
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
|
||||
if (split.Length != 2)
|
||||
throw new ArgumentException("Invalid replacement config");
|
||||
return (split[0], split[1]);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
foreach (var set in _replacementSets)
|
||||
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute to mark a model as json serializable. Used for AOT compilation.
|
||||
/// </summary>
|
||||
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)]
|
||||
public class SerializationModelAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SerializationModelAttribute() { }
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
public SerializationModelAttribute(Type type) { }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializer options
|
||||
/// </summary>
|
||||
public static class SerializerOptions
|
||||
{
|
||||
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
|
||||
|
||||
/// <summary>
|
||||
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
|
||||
{
|
||||
if (!_cache.TryGetValue(typeResolver, out var options))
|
||||
{
|
||||
options = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
Converters =
|
||||
{
|
||||
new DateTimeConverter(),
|
||||
new BoolConverter(),
|
||||
new DecimalConverter(),
|
||||
new IntConverter(),
|
||||
new LongConverter(),
|
||||
new NullableEnumConverterFactory(typeResolver)
|
||||
},
|
||||
TypeInfoResolver = typeResolver,
|
||||
};
|
||||
|
||||
foreach (var converter in additionalConverters)
|
||||
options.Converters.Add(converter);
|
||||
|
||||
options.TypeInfoResolver = typeResolver;
|
||||
_cache.TryAdd(typeResolver, options);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
|
||||
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
|
||||
|
||||
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
|
||||
{
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("");
|
||||
|
||||
reader.Read(); // Start array
|
||||
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||
reader.Read();
|
||||
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||
reader.Read();
|
||||
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||
reader.Read();
|
||||
|
||||
if (reader.TokenType != JsonTokenType.EndArray)
|
||||
throw new Exception("");
|
||||
|
||||
reader.Read(); // End array
|
||||
|
||||
var result = new T();
|
||||
result.QuantityInBaseAsset = baseQuantity;
|
||||
result.QuantityInQuoteAsset = quoteQuantity;
|
||||
result.QuantityInContracts = contractQuantity;
|
||||
return result;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
if (value.QuantityInBaseAsset == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
|
||||
|
||||
if (value.QuantityInQuoteAsset == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
|
||||
|
||||
if (value.QuantityInContracts == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.QuantityInContracts.Value);
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
|
||||
{
|
||||
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("");
|
||||
|
||||
reader.Read(); // Start array
|
||||
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
|
||||
reader.Read();
|
||||
var baseAsset = reader.GetString()!;
|
||||
reader.Read();
|
||||
var quoteAsset = reader.GetString()!;
|
||||
reader.Read();
|
||||
var timeStr = reader.GetString()!;
|
||||
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
|
||||
reader.Read();
|
||||
|
||||
if (reader.TokenType != JsonTokenType.EndArray)
|
||||
throw new Exception("");
|
||||
|
||||
reader.Read(); // End array
|
||||
|
||||
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
writer.WriteStringValue(value.TradingMode.ToString());
|
||||
writer.WriteStringValue(value.BaseAsset);
|
||||
writer.WriteStringValue(value.QuoteAsset);
|
||||
writer.WriteStringValue(value.DeliverTime?.ToString());
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,373 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// System.Text.Json message accessor
|
||||
/// </summary>
|
||||
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// The JsonDocument loaded
|
||||
/// </summary>
|
||||
protected JsonDocument? _document;
|
||||
|
||||
private readonly JsonSerializerOptions? _customSerializerOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsValid { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool OriginalDataAvailable { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? Underlying => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
|
||||
{
|
||||
_customSerializerOptions = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
||||
{
|
||||
if (!IsValid)
|
||||
return new CallResult<object>(GetOriginalString());
|
||||
|
||||
if (_document == null)
|
||||
throw new InvalidOperationException("No json document loaded");
|
||||
|
||||
try
|
||||
{
|
||||
var result = _document.Deserialize(type, _customSerializerOptions);
|
||||
return new CallResult<object>(result!);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||
return new CallResult<object>(new DeserializeError(info, ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
||||
{
|
||||
if (_document == null)
|
||||
throw new InvalidOperationException("No json document loaded");
|
||||
|
||||
try
|
||||
{
|
||||
var result = _document.Deserialize<T>(_customSerializerOptions);
|
||||
return new CallResult<T>(result!);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||
return new CallResult<T>(new DeserializeError(info, ex));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType()
|
||||
{
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
if (_document == null)
|
||||
throw new InvalidOperationException("No json document loaded");
|
||||
|
||||
return _document.RootElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => NodeType.Object,
|
||||
JsonValueKind.Array => NodeType.Array,
|
||||
_ => NodeType.Value
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType(MessagePath path)
|
||||
{
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var node = GetPathNode(path);
|
||||
if (!node.HasValue)
|
||||
return null;
|
||||
|
||||
return node.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => NodeType.Object,
|
||||
JsonValueKind.Array => NodeType.Array,
|
||||
_ => NodeType.Value
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public T? GetValue<T>(MessagePath path)
|
||||
{
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
if (value == null)
|
||||
return default;
|
||||
|
||||
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
try
|
||||
{
|
||||
return value.Value.Deserialize<T>(_customSerializerOptions);
|
||||
}
|
||||
catch { }
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
if (typeof(T) == typeof(string))
|
||||
{
|
||||
if (value.Value.ValueKind == JsonValueKind.Number)
|
||||
return (T)(object)value.Value.GetInt64().ToString();
|
||||
}
|
||||
|
||||
return value.Value.Deserialize<T>(_customSerializerOptions);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||
#endif
|
||||
public T?[]? GetValues<T>(MessagePath path)
|
||||
{
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
if (value == null)
|
||||
return default;
|
||||
|
||||
if (value.Value.ValueKind != JsonValueKind.Array)
|
||||
return default;
|
||||
|
||||
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
|
||||
}
|
||||
|
||||
private JsonElement? GetPathNode(MessagePath path)
|
||||
{
|
||||
if (!IsValid)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
if (_document == null)
|
||||
throw new InvalidOperationException("No json document loaded");
|
||||
|
||||
JsonElement? currentToken = _document.RootElement;
|
||||
foreach (var node in path)
|
||||
{
|
||||
if (node.Type == 0)
|
||||
{
|
||||
// Int value
|
||||
var val = node.Index!.Value;
|
||||
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
|
||||
return null;
|
||||
|
||||
currentToken = currentToken.Value[val];
|
||||
}
|
||||
else if (node.Type == 1)
|
||||
{
|
||||
// String value
|
||||
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
|
||||
return null;
|
||||
currentToken = token;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Property name
|
||||
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (currentToken == null)
|
||||
return null;
|
||||
}
|
||||
|
||||
return currentToken;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string GetOriginalString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System.Text.Json stream message accessor
|
||||
/// </summary>
|
||||
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
|
||||
{
|
||||
private Stream? _stream;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||
{
|
||||
if (bufferStream && stream is not MemoryStream)
|
||||
{
|
||||
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
|
||||
_stream = new MemoryStream();
|
||||
stream.CopyTo(_stream);
|
||||
_stream.Position = 0;
|
||||
}
|
||||
else if (bufferStream)
|
||||
{
|
||||
// We need to buffer the stream, and the current stream is seekable, store as is
|
||||
_stream = stream;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't need to buffer the stream, so don't bother keeping the reference
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||
IsValid = true;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString()
|
||||
{
|
||||
if (_stream is null)
|
||||
throw new NullReferenceException("Stream not initialized");
|
||||
|
||||
_stream.Position = 0;
|
||||
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
|
||||
return textReader.ReadToEnd();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear()
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
_document?.Dispose();
|
||||
_document = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System.Text.Json byte message accessor
|
||||
/// </summary>
|
||||
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
|
||||
{
|
||||
private ReadOnlyMemory<byte> _bytes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
_bytes = data;
|
||||
|
||||
try
|
||||
{
|
||||
var firstByte = data.Span[0];
|
||||
if (firstByte != 0x7b && firstByte != 0x5b)
|
||||
{
|
||||
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError("Not a json value"));
|
||||
}
|
||||
|
||||
_document = JsonDocument.Parse(data);
|
||||
IsValid = true;
|
||||
return CallResult.SuccessResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsValid = false;
|
||||
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString() =>
|
||||
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
||||
#if NETSTANDARD2_0
|
||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
||||
#else
|
||||
Encoding.UTF8.GetString(_bytes.Span);
|
||||
#endif
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear()
|
||||
{
|
||||
_bytes = null;
|
||||
_document?.Dispose();
|
||||
_document = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
|
||||
{
|
||||
private readonly JsonSerializerOptions _options;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
|
||||
{
|
||||
_options = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
#if NET5_0_OR_GREATER
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
||||
#endif
|
||||
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// converter for milliseconds to datetime
|
||||
/// </summary>
|
||||
public class TimestampConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var t = long.Parse(reader.Value.ToString());
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(t);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if(value == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for nanoseconds to datetime
|
||||
/// </summary>
|
||||
public class TimestampNanoSecondsConverter : JsonConverter
|
||||
{
|
||||
private const decimal ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var nanoSeconds = long.Parse(reader.Value.ToString());
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)Math.Round(nanoSeconds * ticksPerNanosecond));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).Ticks / ticksPerNanosecond));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for seconds to datetime
|
||||
/// </summary>
|
||||
public class TimestampSecondsConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
if (reader.Value is double d)
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(d);
|
||||
|
||||
var t = double.Parse(reader.Value.ToString(), CultureInfo.InvariantCulture);
|
||||
// Set ticks instead of seconds or milliseconds, because AddSeconds/AddMilliseconds rounds to nearest millisecond
|
||||
return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddTicks((long)(t * TimeSpan.TicksPerSecond));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalSeconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// converter for datetime string (yyyymmdd) to datetime
|
||||
/// </summary>
|
||||
public class TimestampStringConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
var value = reader.Value.ToString();
|
||||
if (value.Length == 8)
|
||||
return new DateTime(int.Parse(value.Substring(0, 4)), int.Parse(value.Substring(4, 2)), int.Parse(value.Substring(6, 2)), 0, 0, 0, DateTimeKind.Utc);
|
||||
else if(value.Length == 6)
|
||||
return new DateTime(int.Parse(value.Substring(0, 2)), int.Parse(value.Substring(2, 2)), int.Parse(value.Substring(4, 2)), 0, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
throw new Exception("Unknown datetime value: " + value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteValue((DateTime?)null);
|
||||
else
|
||||
{
|
||||
var dateTimeValue = (DateTime)value;
|
||||
writer.WriteValue(int.Parse($"{dateTimeValue.Year}{dateTimeValue.Month}{dateTimeValue.Day}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for utc datetime
|
||||
/// </summary>
|
||||
public class UTCDateTimeConverter: JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(JsonConvert.SerializeObject(value));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
return null;
|
||||
|
||||
DateTime value;
|
||||
if (reader.Value is string s)
|
||||
value = (DateTime)JsonConvert.DeserializeObject(s)!;
|
||||
else
|
||||
value = (DateTime) reader.Value;
|
||||
|
||||
return DateTime.SpecifyKind(value, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,25 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency exchange API's</Description>
|
||||
<PackageVersion>4.2.8</PackageVersion>
|
||||
<AssemblyVersion>4.2.8</AssemblyVersion>
|
||||
<FileVersion>4.2.8</FileVersion>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>10.0.2</PackageVersion>
|
||||
<AssemblyVersion>10.0.2</AssemblyVersion>
|
||||
<FileVersion>10.0.2</FileVersion>
|
||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>4.2.8 - Fixed deadlock in socket receive, Fixed issue in reconnection handling when the client is disconnected again during resubscribing, Added some additional checking of socket state to prevent sending/expecting data when socket is not connected</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="AOT" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
@ -27,25 +37,27 @@
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0">
|
||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.101">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||
<PackageReference Include="System.Text.Json" Version="10.0.1" />
|
||||
<PackageReference Include="NSec.Cryptography" Version="25.4.0" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Client Packages">
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||
<PackageReference Include="System.Threading.Channels" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
24
CryptoExchange.Net/Exceptions/CeDeserializationException.cs
Normal file
24
CryptoExchange.Net/Exceptions/CeDeserializationException.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception during deserialization
|
||||
/// </summary>
|
||||
public class CeDeserializationException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CeDeserializationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CeDeserializationException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,13 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@ -8,6 +16,30 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public static class ExchangeHelpers
|
||||
{
|
||||
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||
private const string _allowedRandomHexChars = "0123456789ABCDEF";
|
||||
|
||||
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
||||
{
|
||||
{ 1, "F" },
|
||||
{ 2, "G" },
|
||||
{ 3, "H" },
|
||||
{ 4, "J" },
|
||||
{ 5, "K" },
|
||||
{ 6, "M" },
|
||||
{ 7, "N" },
|
||||
{ 8, "Q" },
|
||||
{ 9, "U" },
|
||||
{ 10, "V" },
|
||||
{ 11, "X" },
|
||||
{ 12, "Z" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
private static int _lastId;
|
||||
|
||||
/// <summary>
|
||||
/// Clamp a value between a min and max
|
||||
/// </summary>
|
||||
@ -43,15 +75,20 @@ namespace CryptoExchange.Net
|
||||
|
||||
var offset = value % step.Value;
|
||||
if(roundingType == RoundingType.Down)
|
||||
{
|
||||
value -= offset;
|
||||
}
|
||||
else if(roundingType == RoundingType.Up)
|
||||
{
|
||||
if (offset != 0)
|
||||
value += (step.Value - offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (offset < step / 2)
|
||||
value -= offset;
|
||||
else value += (step.Value - offset);
|
||||
}
|
||||
|
||||
value = RoundDown(value, 8);
|
||||
|
||||
return value.Normalize();
|
||||
}
|
||||
@ -75,6 +112,34 @@ namespace CryptoExchange.Net
|
||||
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the provided rules to the value
|
||||
/// </summary>
|
||||
/// <param name="value">Value to be adjusted</param>
|
||||
/// <param name="decimals">Max decimal places</param>
|
||||
/// <param name="valueStep">The value step for increase/decrease value</param>
|
||||
/// <returns></returns>
|
||||
public static decimal ApplyRules(
|
||||
decimal value,
|
||||
int? decimals = null,
|
||||
decimal? valueStep = null)
|
||||
{
|
||||
if (valueStep.HasValue)
|
||||
{
|
||||
var offset = value % valueStep.Value;
|
||||
if (offset != 0)
|
||||
{
|
||||
if (offset < valueStep.Value / 2)
|
||||
value -= offset;
|
||||
else value += (valueStep.Value - offset);
|
||||
}
|
||||
}
|
||||
if (decimals.HasValue)
|
||||
value = Math.Round(value, decimals.Value);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
||||
/// </summary>
|
||||
@ -96,17 +161,23 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds a value down to
|
||||
/// Rounds a value down
|
||||
/// </summary>
|
||||
/// <param name="i"></param>
|
||||
/// <param name="decimalPlaces"></param>
|
||||
/// <returns></returns>
|
||||
public static decimal RoundDown(decimal i, double decimalPlaces)
|
||||
{
|
||||
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
||||
return Math.Floor(i * power) / power;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds a value up
|
||||
/// </summary>
|
||||
public static decimal RoundUp(decimal i, double decimalPlaces)
|
||||
{
|
||||
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
||||
return Math.Ceiling(i * power) / power;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
|
||||
/// </summary>
|
||||
@ -116,5 +187,300 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
return value / 1.000000000000000000000000000000000m;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static int NextId() => Interlocked.Increment(ref _lastId);
|
||||
|
||||
/// <summary>
|
||||
/// Return the last unique id that was generated
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static int LastId() => _lastId;
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of specified length
|
||||
/// </summary>
|
||||
/// <param name="length">Length of the random string</param>
|
||||
/// <returns></returns>
|
||||
public static string RandomString(int length)
|
||||
{
|
||||
var randomChars = new char[length];
|
||||
|
||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||
for (int i = 0; i < length; i++)
|
||||
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
|
||||
#else
|
||||
var random = new Random();
|
||||
for (int i = 0; i < length; i++)
|
||||
randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)];
|
||||
#endif
|
||||
|
||||
return new string(randomChars);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of specified length
|
||||
/// </summary>
|
||||
/// <param name="length">Length of the random string</param>
|
||||
/// <returns></returns>
|
||||
public static string RandomHexString(int length)
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
return "0x" + RandomNumberGenerator.GetHexString(length * 2);
|
||||
#else
|
||||
var randomChars = new char[length * 2];
|
||||
var random = new Random();
|
||||
for (int i = 0; i < length * 2; i++)
|
||||
randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)];
|
||||
return "0x" + new string(randomChars);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a long value
|
||||
/// </summary>
|
||||
/// <param name="maxLength">Max character length</param>
|
||||
/// <returns></returns>
|
||||
public static long RandomLong(int maxLength)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||
var value = RandomNumberGenerator.GetInt32(0, int.MaxValue);
|
||||
#else
|
||||
var random = new Random();
|
||||
var value = random.Next(0, int.MaxValue);
|
||||
#endif
|
||||
var val = value.ToString();
|
||||
if (val.Length > maxLength)
|
||||
return int.Parse(val.Substring(0, maxLength));
|
||||
else
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of specified length
|
||||
/// </summary>
|
||||
/// <param name="source">The initial string</param>
|
||||
/// <param name="totalLength">Total length of the resulting string</param>
|
||||
/// <returns></returns>
|
||||
public static string AppendRandomString(string source, int totalLength)
|
||||
{
|
||||
if (totalLength < source.Length)
|
||||
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
|
||||
|
||||
if (totalLength == source.Length)
|
||||
return source;
|
||||
|
||||
return source + RandomString(totalLength - source.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the month representation for futures symbol based on the delivery month
|
||||
/// </summary>
|
||||
/// <param name="time">Delivery time</param>
|
||||
/// <returns></returns>
|
||||
public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month];
|
||||
|
||||
/// <summary>
|
||||
/// Execute multiple requests to retrieve multiple pages of the result set
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the client</typeparam>
|
||||
/// <typeparam name="U">Type of the request</typeparam>
|
||||
/// <param name="paginatedFunc">The func to execute with each request</param>
|
||||
/// <param name="request">The request parameters</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||
{
|
||||
var result = new List<T>();
|
||||
ExchangeWebResult<T[]> batch;
|
||||
INextPageToken? nextPageToken = null;
|
||||
while (true)
|
||||
{
|
||||
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
||||
yield return batch;
|
||||
if (!batch || ct.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
result.AddRange(batch.Data);
|
||||
nextPageToken = batch.NextPageToken;
|
||||
if (nextPageToken == null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol as retrieved from the exchange</param>
|
||||
/// <param name="quantity">Quantity to trade</param>
|
||||
/// <param name="price">Price to trade at</param>
|
||||
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
|
||||
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
|
||||
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
|
||||
{
|
||||
adjustedPrice = price;
|
||||
adjustedQuantity = quantity;
|
||||
var minNotionalAdjust = false;
|
||||
|
||||
if (price != null)
|
||||
{
|
||||
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
|
||||
adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
|
||||
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
|
||||
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
|
||||
{
|
||||
adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value;
|
||||
minNotionalAdjust = true;
|
||||
}
|
||||
}
|
||||
|
||||
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity);
|
||||
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue updates received from a websocket subscriptions and process them async
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The queued update type</typeparam>
|
||||
/// <param name="subscribeCall">The subscribe call</param>
|
||||
/// <param name="asyncHandler">The async update handler</param>
|
||||
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<T>(
|
||||
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
|
||||
Func<DataEvent<T>, Task> asyncHandler,
|
||||
int? maxQueuedItems = null,
|
||||
QueueFullBehavior? fullBehavior = null)
|
||||
{
|
||||
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||
await processor.StartAsync().ConfigureAwait(false);
|
||||
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
await processor.StopAsync().ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
|
||||
result.Data.SubscriptionStatusChanged += (upd) =>
|
||||
{
|
||||
if (upd == CryptoExchange.Net.Objects.SubscriptionStatus.Closed)
|
||||
_ = processor.StopAsync(true);
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue updates and process them async
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The queued update type</typeparam>
|
||||
/// <param name="subscribeCall">The subscribe call</param>
|
||||
/// <param name="asyncHandler">The async update handler</param>
|
||||
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||
/// <param name="ct">Cancellation token to stop the processing</param>
|
||||
public static async Task ProcessQueuedAsync<T>(
|
||||
Func<Action<T>, Task> subscribeCall,
|
||||
Func<T, Task> asyncHandler,
|
||||
CancellationToken ct,
|
||||
int? maxQueuedItems = null,
|
||||
QueueFullBehavior? fullBehavior = null)
|
||||
{
|
||||
var processor = new ProcessQueue<T>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||
await processor.StartAsync().ConfigureAwait(false);
|
||||
ct.Register(async () =>
|
||||
{
|
||||
await processor.StopAsync().ConfigureAwait(false);
|
||||
});
|
||||
|
||||
await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue updates received from a websocket subscriptions and process them async
|
||||
/// </summary>
|
||||
/// <typeparam name="TEventType">The type of the queued item</typeparam>
|
||||
/// <typeparam name="TOutputType">The type of the item to pass to the processor</typeparam>
|
||||
/// <param name="subscribeCall">The subscribe call</param>
|
||||
/// <param name="mapper">The mapper function to go from <see>TEventType</see> to <see>TOutputType</see></param>
|
||||
/// <param name="asyncHandler">The async update handler</param>
|
||||
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<TEventType, TOutputType>(
|
||||
Func<ProcessQueue<DataEvent<TEventType>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
|
||||
Func<DataEvent<TEventType>, DataEvent<TOutputType>> mapper,
|
||||
Func<DataEvent<TOutputType>, Task> asyncHandler,
|
||||
int? maxQueuedItems = null,
|
||||
QueueFullBehavior? fullBehavior = null
|
||||
)
|
||||
{
|
||||
var processor = new ProcessQueue<DataEvent<TEventType>>((update) => {
|
||||
return asyncHandler.Invoke(mapper.Invoke(update));
|
||||
}, maxQueuedItems, fullBehavior);
|
||||
await processor.StartAsync().ConfigureAwait(false);
|
||||
var result = await subscribeCall(processor).ConfigureAwait(false);
|
||||
if (!result)
|
||||
{
|
||||
await processor.StopAsync().ConfigureAwait(false);
|
||||
return result;
|
||||
}
|
||||
|
||||
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
|
||||
result.Data.SubscriptionStatusChanged += (upd) =>
|
||||
{
|
||||
if (upd == SubscriptionStatus.Closed)
|
||||
_ = processor.StopAsync(true);
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a decimal value from a string
|
||||
/// </summary>
|
||||
public static decimal? ParseDecimal(string? value)
|
||||
{
|
||||
// Value is null or empty is the most common case to return null so check before trying to parse
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return null;
|
||||
|
||||
// Try parse, only fails for these reasons:
|
||||
// 1. string is null or empty
|
||||
// 2. value is larger or smaller than decimal max/min
|
||||
// 3. unparsable format
|
||||
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
|
||||
return decValue;
|
||||
|
||||
// Check for values which should be parsed to null
|
||||
if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Infinity value should be parsed to min/max value
|
||||
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||
return decimal.MaxValue;
|
||||
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||
return decimal.MinValue;
|
||||
|
||||
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
|
||||
{
|
||||
// Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly.
|
||||
// assume overflow
|
||||
if (overflowValue < 0)
|
||||
return decimal.MinValue;
|
||||
else
|
||||
return decimal.MaxValue;
|
||||
}
|
||||
|
||||
// Unknown decimal format, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common balance
|
||||
/// </summary>
|
||||
public interface ICommonBalance
|
||||
{
|
||||
/// <summary>
|
||||
/// The asset name
|
||||
/// </summary>
|
||||
public string CommonAsset { get; }
|
||||
/// <summary>
|
||||
/// Amount available
|
||||
/// </summary>
|
||||
public decimal CommonAvailable { get; }
|
||||
/// <summary>
|
||||
/// Total amount
|
||||
/// </summary>
|
||||
public decimal CommonTotal { get; }
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Common trade
|
||||
/// </summary>
|
||||
public interface ICommonTrade
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the trade
|
||||
/// </summary>
|
||||
public string CommonId { get; }
|
||||
/// <summary>
|
||||
/// Price of the trade
|
||||
/// </summary>
|
||||
public decimal CommonPrice { get; }
|
||||
/// <summary>
|
||||
/// Quantity of the trade
|
||||
/// </summary>
|
||||
public decimal CommonQuantity { get; }
|
||||
/// <summary>
|
||||
/// Fee paid for the trade
|
||||
/// </summary>
|
||||
public decimal CommonFee { get; }
|
||||
/// <summary>
|
||||
/// The asset fee was paid in
|
||||
/// </summary>
|
||||
public string? CommonFeeAsset { get; }
|
||||
/// <summary>
|
||||
/// Trade time
|
||||
/// </summary>
|
||||
DateTime CommonTradeTime { get; }
|
||||
}
|
||||
}
|
||||
@ -1,177 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.ExchangeInterfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Shared interface for exchange wrappers based on the CryptoExchange.Net package
|
||||
/// </summary>
|
||||
public interface IExchangeClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Should be triggered on order placing
|
||||
/// </summary>
|
||||
event Action<ICommonOrderId> OnOrderPlaced;
|
||||
/// <summary>
|
||||
/// Should be triggered on order cancelling
|
||||
/// </summary>
|
||||
event Action<ICommonOrderId> OnOrderCanceled;
|
||||
|
||||
/// <summary>
|
||||
/// Get the symbol name based on a base and quote asset
|
||||
/// </summary>
|
||||
/// <param name="baseAsset"></param>
|
||||
/// <param name="quoteAsset"></param>
|
||||
/// <returns></returns>
|
||||
string GetSymbolName(string baseAsset, string quoteAsset);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of symbols for the exchange
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonSymbol>>> GetSymbolsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of tickers for the exchange
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonTicker>>> GetTickersAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get a ticker for the exchange
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get klines for</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonTicker>> GetTickerAsync(string symbol);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of candles for a given symbol on the exchange
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to retrieve the candles for</param>
|
||||
/// <param name="timespan">The timespan to retrieve the candles for. The supported value are dependent on the exchange</param>
|
||||
/// <param name="startTime">[Optional] Start time to retrieve klines for</param>
|
||||
/// <param name="endTime">[Optional] End time to retrieve klines for</param>
|
||||
/// <param name="limit">[Optional] Max number of results</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonKline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null);
|
||||
/// <summary>
|
||||
/// Get the order book for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get the book for</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonOrderBook>> GetOrderBookAsync(string symbol);
|
||||
/// <summary>
|
||||
/// The recent trades for a symbol
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get the trades for</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonRecentTrade>>> GetRecentTradesAsync(string symbol);
|
||||
|
||||
/// <summary>
|
||||
/// Place an order
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol the order is for</param>
|
||||
/// <param name="side">The side of the order</param>
|
||||
/// <param name="type">The type of the order</param>
|
||||
/// <param name="quantity">The quantity of the order</param>
|
||||
/// <param name="price">The price of the order, only for limit orders</param>
|
||||
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns>The id of the resulting order</returns>
|
||||
Task<WebCallResult<ICommonOrderId>> PlaceOrderAsync(string symbol, OrderSide side, OrderType type, decimal quantity, decimal? price = null, string? accountId = null);
|
||||
/// <summary>
|
||||
/// Get an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonOrder>> GetOrderAsync(string orderId, string? symbol = null);
|
||||
/// <summary>
|
||||
/// Get trades for an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonTrade>>> GetTradesAsync(string orderId, string? symbol = null);
|
||||
/// <summary>
|
||||
/// Get a list of open orders
|
||||
/// </summary>
|
||||
/// <param name="symbol">[Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonOrder>>> GetOpenOrdersAsync(string? symbol = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of closed orders
|
||||
/// </summary>
|
||||
/// <param name="symbol">[Optional] The symbol to get closed orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonOrder>>> GetClosedOrdersAsync(string? symbol = null);
|
||||
/// <summary>
|
||||
/// Cancel an order by id
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<ICommonOrderId>> CancelOrderAsync(string orderId, string? symbol = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get balances
|
||||
/// </summary>
|
||||
/// <param name="accountId">[Optional] The account id to retrieve balances for, required for some exchanges, ignored otherwise</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<ICommonBalance>>> GetBalancesAsync(string? accountId = null);
|
||||
|
||||
/// <summary>
|
||||
/// Common order id
|
||||
/// </summary>
|
||||
public enum OrderType
|
||||
{
|
||||
/// <summary>
|
||||
/// Limit type
|
||||
/// </summary>
|
||||
Limit,
|
||||
/// <summary>
|
||||
/// Market type
|
||||
/// </summary>
|
||||
Market,
|
||||
/// <summary>
|
||||
/// Other order type
|
||||
/// </summary>
|
||||
Other
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Common order side
|
||||
/// </summary>
|
||||
public enum OrderSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Buy order
|
||||
/// </summary>
|
||||
Buy,
|
||||
/// <summary>
|
||||
/// Sell order
|
||||
/// </summary>
|
||||
Sell
|
||||
}
|
||||
/// <summary>
|
||||
/// Common order status
|
||||
/// </summary>
|
||||
public enum OrderStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// placed and not fully filled order
|
||||
/// </summary>
|
||||
Active,
|
||||
/// <summary>
|
||||
/// cancelled order
|
||||
/// </summary>
|
||||
Canceled,
|
||||
/// <summary>
|
||||
/// filled order
|
||||
/// </summary>
|
||||
Filled
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user