From 3c15e7a531a24179e31ba2bc9c9dcf408a15c242 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 22 Mar 2019 09:34:49 +1000 Subject: [PATCH 01/10] Dev version bump [skip ci] --- src/Seq.Api/Seq.Api.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Seq.Api/Seq.Api.csproj b/src/Seq.Api/Seq.Api.csproj index 677b359..86307f7 100644 --- a/src/Seq.Api/Seq.Api.csproj +++ b/src/Seq.Api/Seq.Api.csproj @@ -1,7 +1,7 @@ - + Client library for the Seq HTTP API. - 5.1.0 + 5.1.1 Datalust;Contributors netstandard2.0 $(NoWarn);CS1591 @@ -21,4 +21,4 @@ - \ No newline at end of file + From d881e9ef05bc1f2c6cf0112e1b2fa3d60aec3e6c Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 14:17:28 +1000 Subject: [PATCH 02/10] Retry loading resource groups if the initial attempt fails; fixes #59 --- asset/getseq_net.ico | Bin 99678 -> 0 bytes example/SeqQuery/App.config | 14 -------------- example/SeqQuery/SeqQuery.csproj | 4 ---- example/SeqQuery/getseq_net.ico | Bin 99678 -> 0 bytes example/SeqTail/App.config | 14 -------------- example/SeqTail/SeqTail.csproj | 4 ---- example/SeqTail/getseq_net.ico | Bin 99678 -> 0 bytes example/SignalCopy/App.config | 14 -------------- example/SignalCopy/SignalCopy.csproj | 4 ---- example/SignalCopy/getseq_net.ico | Bin 99678 -> 0 bytes seq-api.sln | 12 +++++------- seq-api.sln.DotSettings | 4 +++- src/Seq.Api/Seq.Api.csproj | 5 +---- src/Seq.Api/SeqConnection.cs | 15 +++++++++++++-- 14 files changed, 22 insertions(+), 68 deletions(-) delete mode 100644 asset/getseq_net.ico delete mode 100644 example/SeqQuery/App.config delete mode 100644 example/SeqQuery/getseq_net.ico delete mode 100644 example/SeqTail/App.config delete mode 100644 example/SeqTail/getseq_net.ico delete mode 100644 example/SignalCopy/App.config delete mode 100644 example/SignalCopy/getseq_net.ico diff --git a/asset/getseq_net.ico b/asset/getseq_net.ico deleted file mode 100644 index 34b3b9f29490c71557d990db1c7a24e6958fccd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99678 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 Mlz=ILz?H!N1JN%4H2?qr diff --git a/example/SeqQuery/App.config b/example/SeqQuery/App.config deleted file mode 100644 index 8277a73..0000000 --- a/example/SeqQuery/App.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/example/SeqQuery/SeqQuery.csproj b/example/SeqQuery/SeqQuery.csproj index 40bca25..ea3a4f4 100644 --- a/example/SeqQuery/SeqQuery.csproj +++ b/example/SeqQuery/SeqQuery.csproj @@ -6,10 +6,6 @@ Exe - - - - diff --git a/example/SeqQuery/getseq_net.ico b/example/SeqQuery/getseq_net.ico deleted file mode 100644 index 34b3b9f29490c71557d990db1c7a24e6958fccd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99678 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 Mlz=ILz?H!N1JN%4H2?qr diff --git a/example/SeqTail/App.config b/example/SeqTail/App.config deleted file mode 100644 index 8277a73..0000000 --- a/example/SeqTail/App.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/example/SeqTail/SeqTail.csproj b/example/SeqTail/SeqTail.csproj index 3ceeb51..ac34313 100644 --- a/example/SeqTail/SeqTail.csproj +++ b/example/SeqTail/SeqTail.csproj @@ -6,10 +6,6 @@ Exe - - - - diff --git a/example/SeqTail/getseq_net.ico b/example/SeqTail/getseq_net.ico deleted file mode 100644 index 34b3b9f29490c71557d990db1c7a24e6958fccd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99678 zcmeHQX_OR2x^2&SbI$wq&iql|y!Xa+oN+@?5mAwShh`IyMOg#^1r=8WL|e4kn;k(A zMci=TM;#SKZ~;{IeG#Q;79FLlak_QryzhRQ60LHRozvc(dXQbsL067h`3)w zMtre+IXS1~{2}L$f6U?ESve#ABPXY~_|9^EuVDB}hBasq{(W9f&ahK+a+)`HexKhm zC+EMe$;rvf3;+JlQ*s(j$jLdMKjo2U5vSPyc@Lix`-f#6fMp#M!`&YOw`{{314@9! z%_3OR9PopY?*W50{Y}^5@%jO}zu+&x)IU3A z<(2;z`vM<~1wNYw>{|)EbR+QE&A@y20n0lBU(5v-Tps`ackTezJ`QYq1NiL(?;Q%f zJP_DLSzS4i&z%m;s~`XVr7eLMt_NNn0(_DWJku3ebd@)^Ug=Ln4pMl3P7Y8VNIP>D z(VS52OrG7AF+lsn)8wE^dlp!Y@>Yq}I+N?X2vPQAo1_Yxo(+`@LQ#|9Ux zI~+oO3%-Bm`O%og-|4{3vw$w&xLDPu5EGm5{WH$-iE|mR8W=nS=-)wb(m?{~95+{tM>SM&wmzRei}&HSr#?OXPBpY0aiw)%a)+_#3` z>LT`4UcCvhEXO@@%<->ar*+j7;3)gDFWmshyWYN?eaLHo?Qc5%@4raj`(>>6)+k{2 z`%W2s|0Uo17O?q6;J_N^TmB{cTItHa=tTB@?DUZe&f~c7bztYaPG4BYg^~st8!l~? zp7NJI_u(zThG(OnujDMA>zDs{_g6REyx)9(Jib4gDxg-pqNflQ>L9|^Q&5*0`)BJP zmgm9>19U5c#FOiK{IvO}P6I}5g*9rs4TRl;BMxB6fO0Gvz&700=Za1@5wNXGR0o_) z{7D>}+_Kfy9gF^0;fJB$VNw5bEa=B^!jl{<6P5?`aNX|}x3Vz(UeOC#)a3|7r&Fmv zpJSWhTVM?%2L!_I4>x^>$NH3G-t{bh`5q8f80vRCSK`IS*Y8cKf8wLS(9N)hG7kj8 z?)L-tV}7r4%<0AS>-zk7em^??bi~Pf7#K|54(ASB4Bmq8Z`g;q-ODknJLP8)l?{aD z0XnuymZ zGIum~9ME@zJ$EGYM+)Ed+>3(t<(S4M0P`=iVbQfQFYLyIOFTDDf`0wpg#2Uf2YP)A ztJek_=tVB*-`{Nyrnf1_uvA@%9 zOm21(k2ItF%>0`Ti*79BnhMkXD$K8|-t*?K>d#AsyNn+zrd0?IzrsE_;0@h=*aYU{MrV>?%vkBFtPDTJk*%!k2{|mZ=Sb0 z;c@1NwXpH*VXU!_MgFY zoWXZF5Z$>YxOF7;+UsDoCIj|)UkZna#8JxzAv!TKjLu0GRW-SKZa@4xwGt{eD@y+~ z0rH^I@66L^pa__(py*zx@gGrP`FL>ZxjTq}}r8kJApTcF%rJoH$B; zq}>VY*u-_~uibuX?GM|y3hCGKCvA6y>)|%E@^JmW1pIFMBkfKEk@U0w6WfN{-!av; zt8ba)OZThqnyRM}E}ZY39}O`i=akGFBW_loMTMbiXR|O60`lB_@tr{ziUm+1BccBkS?&N%VDx z164mF*QyB5hW6*Hg|K8DsaLyHUHOr@idoc8$U4NQJKAGq>-k>J9IA}JESa|rx2aKWl$-t}+n=8gIBRTo ze*hez{$1wH%gce1qs~07T$kShNwdt~h7JS2e9t+BHqQFpL*D>D?FA&R%zMii^^18x zaS`Xbw*x1Ba^{`GbvR1Ksw+QChj44{{)e4@i|M!Q^dE!8wRqOgOeg2zCCr+4sl8^| zq8`=S!}a!D*KDn&Y;1ag`la)n`32GMaX#KEEr#_(3tP`={rsI){ztDxtUvJLezXAOEPnQ24=UH-}aB#dZ zEMJVj?~`!(d$a{rl|R*b(ym9^1ML~o)`ZiKaQR2%$4S3^UGgK4^uIpLN$&^LyGng{ z_bx~7UK!-9huHN#WvYQAJF@QlALD`2Q1bG_^vk#%QGFh#>{a=D$&bZyRPtlXj>wP5 z6U*6R+NJ(l0zPdgKe7{RSW9 zIcq@Wy2!yQV#@zIYlA+V;IywIKdx-*GMa$jNBS$3A6>4LUtHD=TVLeIA1>btvf(G( zxBN(&BC;1(|onIsVep@AQXR)*>5r`8efX!7uHqEk7bdKKznz_n8U$UHJ+3AE>{I8vn}pODk8^ z_lV}#$&VAy$y20WN|~xKZ@(`&iAsY!$AM};ZM~t__*W}`bx)Kl<*S~p`&C%FT|e@p z%c7t3sC^?lKQ{fg{xed4MgQrn2QqGLm4mF)YEI-4YMoXN_qfLX zIcu`IaP3tc!Q6X*+qtHKYr1e-MVuJ98_y0Z!=iy)zCmDFr|P51{dk>y?Ac3EoZ0MK zk7E9BW!^n-V#F>iA5ezJZeSOTNNjyuhs=*ZYc$uxZ3ISa^Tdf^JMnb?GR*JGN#l4fMYZ=J# z{*BDP2TJ;H$KtMKn32mRNkqK$kTn~AHPewIk4RF12${=$MwT-bonQ!L5BlGWp;`W;`^XgKrafXfbu5&&+{@GXY);9Y--{Tze z=7BNn)OPp+XwN>72a4NnM8TD%n9v9|(wqOYYIq$v-#4Zk^UeIXW&S--+kkq+z$(&VJQ}^7-PJN5j_PKa+HeM`>uibzt^-FPo zJ=k#PLHx`yWJ8?M?SbaZzcw$V`-_`wz~p+R_8R{hJx`~m%Fg89@NeYb)PK|ddCGuk z|IPQ$eE&`VNBVxI|0m<0)9MyX|JU??P5;-k?<3=2)BiF3AJhLa{U6i+$-@3$`AKJA zOl5txbo77ZY?*0iI{ifD7uVj~$Z)wP^8%7CnNN}5HqJ(1L)PP{ zYjSq*LC)dGeoEOBCC`(6QR=#vn-2cvooX-NhG!iA2jiT*uWFy4oa-y;Ikv;OuEJ$L z_#o#Y{$1vr{rl@rz>m9}xN;7&?AzS=4)E0iXaAyv)qGd(N%p1dj9h3>#o2>2?m-}Vn8eHp>6kPcr=>=R2^dvGa{oWgvAz&SIDEgY0pg8vc&T zb&&(PCbqQXJb|wsbM}9$d%fI@Yya8)t(gg&{2h=z_p)zY^5m8FRD@+>{-qBf{7U_o zIxpwZrXsIi&qzo9z21|lVJ81Z|7Y}nM*nB#zddEZ=zop=*XVzZ{#WULW6uLHzk^i* za!y2^Wcd{BoCw-JkNvaHi4Z$&2ANp~Pxhfco;st^-1nfLdvQDUu|9F^*8O;MNC_Sv zWD(q=tf4a7P3Br|rM9hVq))KYMbMo zYvzoTu${0PDbs9vm++rY{%6Y#V}NM^GW-^PlPbq&s~}IG2_J{$IPHgy&*e?)g~m zIa##tC^jAqe3n7F`Hp?Q@IR6D|5n!j;oFkJ(II>BYJc+Ihx~Imp~Ie^mN{wze`Qe9 zb!9Qvm^@s-v+uap#^c;KKVe62+KX5EmDu)I)%kGBHN0oI>_1N3Ox_HB`^o>!TsM;h zjt(aOeM>O6w?%Ndf`)i@cTIRJ+wO4fl|yT%JyQG+C;uMj9{LG8I*9!DA^)^rtjBpb;a?0>?JFA=jh182on991(v#)5&yGnSR4SHn29=y~mgxNhTg6D&3@MX_@ zakJ#l2K6H&CQkhGY9H$eJTQfIn*0wU|A~S!bL2+y-!sH>Q7!vyR5>42$w1=g#5Q%- zx!Y&PrpjLp`Kpd=X2ZP;sG}M`nP>if>3QyLyz_AN@WcIgk#ofCZ(Ww5r9p><#$^%KJXK=SW# z?zNw=!+pqqSMuMP{Bt^2N9~t;ZS$Vt@1Msp;QfaGz{;u3|D$aCdG@Ktx$l0$4)@-L z7ji?G-q9ks-Bkl`P>7er9uwc+bN?-_L3PVPoa+ zu!|wKGK#L-h37hkXcxnB?PJu8os1;pDj8X<%lFLUp7MM0{P7=0K8kwm!m}Mhn9|lF zc%+AhI%6MY{$+K~=8NP>_`jR{U(fpAll4Cd6m=v2?LwH`+9J4|TtixIwxrV@jC!`% zfv?M$*#x-jA&39#$bS+j>beWhv<+cWD~sTAat)YUj|&2_8J9TcFUVOuo~Y08EJgv( zhq;sINAy|~*Y;7YEu`4rMO}8{nKmKXMY3G`NUoivtmZzO<<@7~3wWkPD)?_r{#%fL zPM7PbGxo9Sik+|6e9YiEL8BkwcYn2~-7ha&V6kCExjV4DRWa>?S+0FB*G|~%3pp8V zkp42bh~=CFrZ?i=ukrlulmBktCWWHTJMeUiVm#8+BDkDfL-yH-yMTS-0AX4~?)@6a zZ~tesd-si}?K>}fczW2OPCKxyc`@zcS+0G&h@HIixga1x%wl|lnkj>6)S=u){=1QX zvHO>sI1Y8#j%Cfre-rZ0;e-y%{44oS6o0E*wiPqkR^(Lb07i{vyN_tDE1WNg@> z_S^AP(_-3%v|Rg;uARu*w(A)0{RzutqB5K%@uyP1F_QduA^)9N_mjY(cH8mfwZ+K0 z$|AU(KtsxGgr?Hos5~p4Tm&tAN(1iwx}WVn`S-AU>L;wQ?RG4^rkHk7E!RG(YbSNa z*<28kjX0O-4ge-ouXW3PZ2Lc@{a5Y*PXdK)wqfbj#dz>ai$E0WoSj%X%R5!bgnBWl z9`}CT%XXjqw|8Jkhuq6RxlbZYR)?va(85nP<{-4F9RCOPR!LHl=0K#ZLY2 z$olVL_u5ZbVN2HkD_Q>=vi?(@pu;_*UY%8M+{n58DJhFH`Mhc%pE|?g_wc(<{@bwq zPXdK4wqZ%5VmxrUMQ}Tj2HReN*eo!}fHL@VMcI3ly7^(`zx^i_Y`(|ZfRFdPusQj^ zqL_B!E!RH0YbRdpy(kzN$N(9Lsf&+N*E;O(>ha%<{5K^3my&;~6LetmpSU0-g;26& z+(^BuPFYN(o@&UQZ1-0JE!O!*%k@AQ2b*rg;>(LMuD(TZJDCRC)|1$!G04m^c$m7d zn?|$k|Cs0hlm8@eu*p_De%W!1y~HB8ok&C4>{TRfN8)rYP%?1KAdfn(!FK?y$bU2P zp9BtGvlWXP9LN0^TLiZgX~;bOPfMQ-m84~|7Soh>j>CUT@*l_cK~>=guO|QXkJB!; zW!uJ9XN1>DJK5RDab;;X{0F-|F#H?-<@;~i|6t02ssF*`UG)hJ|Av3l{s&hEO#Kfo z_wo%e{2TsF`yX5xF!evU+?)14*xVcb4gaS754H>#{tf@8{~uf#F!evU+?)Piu(>z< z8~#oEA8Z*g{2TsF|3A1gVCsKxxi|g4U~_NyH~gFSKiD#0_&5BU{(o>~z|{ZXa&P*7 z!RFrZZ}>Ouf3Ri1@Nf7x{r}*~fT{n%<=*uFg3Z0*-|%nR|6t32;otCY`v1X|0aO2j z%f0FUWiR(9e+Mkff$*3tO#M&3?2;|>RNQCy&raS_!M!{qi2Um|VO2TGx$3;oLDt@N8LF8ZZtRObUg^Oh!B4Kjvr`^su`*Q6=+BH}4 zb@%0g5&pNm<{d}HpHKU!I_FQe0ls}2IIRxeUWhar}+0_5n z^IVhsOMgMydiDNL5%A9K!1FzU-5*54g)>$53mQhZ71DM~doTTs#Z7^Ai=8?wZM)QU zb)UE!pNl;6w-dm}dCol&Po5JiQ~JG$`VIe)vR3@+u4@0EXaN+`KDM;w;#cJ?wjT95 zT|j#}a!uNG^&IIhsQ9`&m$p~+-{O~VzW$BTzf~&km-8dfmUpP|WL(34bXh9Sl&koc zvX$?<^f&c#mU^z&N0A4)eqgopT)DP9*Wp#TjR$ELiox+uW%;)c@m^k#bb!uDf3ROFvHZ40=4Nt5WY(nG1L7y7cGXySF0rIBExTgL`m$j;o%GK+?t}B%Ow~WgxD{~nq{`zC(&lh=}*C6^i zGH(&vw@~R3S9AjU@5<6p)iuL^bXn@=9`gl#A`|mP9 zc2&P*!qF#P-DU&>S3U#a^#=i--sfz0RV z>q`8Ve{IJz{8!{t=QsAdF5hF(+sGLGU%$X!mm%MR+U7CY_h%FT$>!1A7hgUN|MANv z2y$ciH~gFZU%{0DQ~!g@y?oyc|Av3l{s&hEO#Kfo_on?1Hur{q!@p_&gDnGwf5X4& z{|8qFO#Kfo_on|BZ0-&JhJVxk2U`XV|Av3l{|~MVnED@F?rWg`mt+1+378TvC16Ux Mlz=ILz?H!N1JN%4H2?qr diff --git a/example/SignalCopy/App.config b/example/SignalCopy/App.config deleted file mode 100644 index 8277a73..0000000 --- a/example/SignalCopy/App.config +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/example/SignalCopy/SignalCopy.csproj b/example/SignalCopy/SignalCopy.csproj index 3f1644b..9b9700e 100644 --- a/example/SignalCopy/SignalCopy.csproj +++ b/example/SignalCopy/SignalCopy.csproj @@ -6,10 +6,6 @@ Exe - - - - diff --git a/example/SignalCopy/getseq_net.ico b/example/SignalCopy/getseq_net.ico deleted file mode 100644 index 34b3b9f29490c71557d990db1c7a24e6958fccd4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 99678 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 Mlz=ILz?H!N1JN%4H2?qr diff --git a/seq-api.sln b/seq-api.sln index 9abcb17..0c8feca 100644 --- a/seq-api.sln +++ b/seq-api.sln @@ -1,17 +1,12 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26403.3 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29230.47 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B36BE973-6D5B-4DAA-BF42-F1CF11D6F18E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "example", "example", "{1C66E116-DC21-4C8F-833E-A4C2DDF58487}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "asset", "asset", "{233C7E7D-26DE-452A-A415-44F99CBA18E7}" - ProjectSection(SolutionItems) = preProject - asset\getseq_net.ico = asset\getseq_net.ico - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{7DCED657-7E83-4E6F-AB14-5753E044304D}" ProjectSection(SolutionItems) = preProject appveyor.yml = appveyor.yml @@ -69,4 +64,7 @@ Global {42CEBFBA-208F-40F1-AC95-13F05F6D5412} = {1C66E116-DC21-4C8F-833E-A4C2DDF58487} {E8CDDE17-8E29-4EB4-A4BB-38BCE346A752} = {1C66E116-DC21-4C8F-833E-A4C2DDF58487} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {20BAB483-FB94-4373-8E4C-0F846B6DBFFC} + EndGlobalSection EndGlobal diff --git a/seq-api.sln.DotSettings b/seq-api.sln.DotSettings index d50635d..832d668 100644 --- a/seq-api.sln.DotSettings +++ b/seq-api.sln.DotSettings @@ -1,2 +1,4 @@  - AD \ No newline at end of file + AD + True + True \ No newline at end of file diff --git a/src/Seq.Api/Seq.Api.csproj b/src/Seq.Api/Seq.Api.csproj index 86307f7..11f7d45 100644 --- a/src/Seq.Api/Seq.Api.csproj +++ b/src/Seq.Api/Seq.Api.csproj @@ -7,14 +7,11 @@ $(NoWarn);CS1591 true true - Seq.Api - Seq.Api seq Copyright © 2014-2019 Datalust Pty Ltd and Contributors https://datalust.co/images/seq-nuget.png https://github.com/datalust/seq-api - http://www.apache.org/licenses/LICENSE-2.0 - Seq.Api + Apache-2.0 7.3 diff --git a/src/Seq.Api/SeqConnection.cs b/src/Seq.Api/SeqConnection.cs index 6d3e722..487aa20 100644 --- a/src/Seq.Api/SeqConnection.cs +++ b/src/Seq.Api/SeqConnection.cs @@ -60,9 +60,20 @@ public SeqConnection(string serverUrl, string apiKey = null, bool useDefaultCred public WorkspacesResourceGroup Workspaces => new WorkspacesResourceGroup(this); - public async Task LoadResourceGroupAsync(string name, CancellationToken cancellationToken = default) + public Task LoadResourceGroupAsync(string name, CancellationToken cancellationToken = default) { - return await _resourceGroups.GetOrAdd(name, s => ResourceGroupFactory(s, cancellationToken)).ConfigureAwait(false); + // Initially, we want to put an incomplete task into the cache so that any concurrent attempts to load the + // same resource group will wait on the same pending call. + var cached = _resourceGroups.GetOrAdd(name, s => ResourceGroupFactory(s, cancellationToken)); + + if (!cached.IsFaulted && !cached.IsCanceled) + return cached; + + // If the cached task failed (ideally a rare situation), clobber it and return a new task (less worried about + // overlapping/concurrent calls on this path). + return _resourceGroups.AddOrUpdate(name, + s => ResourceGroupFactory(s, cancellationToken), + (s, _) => ResourceGroupFactory(s, cancellationToken)); } async Task ResourceGroupFactory(string name, CancellationToken cancellationToken = default) From ca942b9262ae1e8ec1bdb200c9c413990b6988b2 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 14:24:50 +1000 Subject: [PATCH 03/10] Include 'render' and 'permalinkId' parameters in single event load requests --- seq-api.sln.DotSettings | 4 +++- src/Seq.Api/ResourceGroups/EventsResourceGroup.cs | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/seq-api.sln.DotSettings b/seq-api.sln.DotSettings index d50635d..2061b34 100644 --- a/seq-api.sln.DotSettings +++ b/seq-api.sln.DotSettings @@ -1,2 +1,4 @@  - AD \ No newline at end of file + AD + True + True \ No newline at end of file diff --git a/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs b/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs index cca68e7..c8b736c 100644 --- a/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs +++ b/src/Seq.Api/ResourceGroups/EventsResourceGroup.cs @@ -34,6 +34,8 @@ public async Task FindAsync( if (id == null) throw new ArgumentNullException(nameof(id)); var parameters = new Dictionary {{"id", id}}; + if (render) parameters.Add("render", true); + if (permalinkId != null) parameters.Add("permalinkId", permalinkId); return await GroupGetAsync("Item", parameters, cancellationToken).ConfigureAwait(false); } From 59c3e3defb1a27fed86dab6a2d02c8254e2bd02d Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 15:08:28 +1000 Subject: [PATCH 04/10] Add EnsureConnected(); be more resilient to failed initial root entity loading --- example/SeqQuery/Program.cs | 2 +- example/SeqTail/Program.cs | 6 +-- seq-api.sln.DotSettings | 6 ++- src/Seq.Api/Client/SeqApiClient.cs | 11 ++-- src/Seq.Api/SeqConnection.cs | 86 +++++++++++++++++++++++------- 5 files changed, 80 insertions(+), 31 deletions(-) diff --git a/example/SeqQuery/Program.cs b/example/SeqQuery/Program.cs index c80ac00..9a793a7 100644 --- a/example/SeqQuery/Program.cs +++ b/example/SeqQuery/Program.cs @@ -32,7 +32,7 @@ static void Main(string[] args) var server = arguments[""].ToString(); var query = Normalize(arguments[""]); var apiKey = Normalize(arguments["--apikey"]); - var @from = Normalize(arguments["--from"]); + var from = Normalize(arguments["--from"]); var to = Normalize(arguments["--to"]); await Run(server, apiKey, query, from, to); diff --git a/example/SeqTail/Program.cs b/example/SeqTail/Program.cs index 162fdd1..0d98f85 100644 --- a/example/SeqTail/Program.cs +++ b/example/SeqTail/Program.cs @@ -46,7 +46,7 @@ static void Main(string[] args) Console.WriteLine("Tailing, press Ctrl+C to exit."); Console.CancelKeyPress += (s,a) => cancel.Cancel(); - var run = Task.Run(() => Run(server, apiKey, filter, cancel)); + var run = Task.Run(() => Run(server, apiKey, filter, cancel), cancel.Token); run.GetAwaiter().GetResult(); } @@ -78,8 +78,8 @@ static async Task Run(string server, string apiKey, string filter, CancellationT using (var stream = await connection.Events.StreamAsync(filter: strict)) { var subscription = stream - .Select(jObject => LogEventReader.ReadFromJObject(jObject)) - .Subscribe(evt => Log.Write(evt), () => cancel.Cancel()); + .Select(LogEventReader.ReadFromJObject) + .Subscribe(Log.Write, cancel.Cancel); cancel.Token.WaitHandle.WaitOne(); subscription.Dispose(); diff --git a/seq-api.sln.DotSettings b/seq-api.sln.DotSettings index 0cbd2ce..969cc30 100644 --- a/seq-api.sln.DotSettings +++ b/seq-api.sln.DotSettings @@ -1,7 +1,11 @@ - + AD + True + True True True True + True + True True diff --git a/src/Seq.Api/Client/SeqApiClient.cs b/src/Seq.Api/Client/SeqApiClient.cs index 3afd2c7..e59174f 100644 --- a/src/Seq.Api/Client/SeqApiClient.cs +++ b/src/Seq.Api/Client/SeqApiClient.cs @@ -25,10 +25,9 @@ public class SeqApiClient : IDisposable // Future versions of Seq may not completely support v1 features, however // providing this as an Accept header will ensure what compatibility is available - // can be utilised. + // can be utilized. const string SeqApiV7MediaType = "application/vnd.datalust.seq.v7+json"; - readonly HttpClient _httpClient; readonly CookieContainer _cookies = new CookieContainer(); readonly JsonSerializer _serializer = JsonSerializer.Create( new JsonSerializerSettings @@ -53,12 +52,12 @@ public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCrede if (!baseAddress.EndsWith("/")) baseAddress += "/"; - _httpClient = new HttpClient(handler) { BaseAddress = new Uri(baseAddress) }; + HttpClient = new HttpClient(handler) { BaseAddress = new Uri(baseAddress) }; } public string ServerUrl { get; } - public HttpClient HttpClient => _httpClient; + public HttpClient HttpClient { get; } public Task GetRootAsync(CancellationToken cancellationToken = default) { @@ -183,7 +182,7 @@ async Task HttpSendAsync(HttpRequestMessage request, CancellationToken c request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(SeqApiV7MediaType)); - var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); if (response.IsSuccessStatusCode) @@ -239,7 +238,7 @@ static string ResolveLink(ILinked entity, string link, IDictionary> _resourceGroups = new ConcurrentDictionary>(); - readonly Lazy> _root; + readonly object _sync = new object(); + readonly Dictionary> _resourceGroups = new Dictionary>(); + Task _root; public SeqConnection(string serverUrl, string apiKey = null, bool useDefaultCredentials = true) { if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl)); Client = new SeqApiClient(serverUrl, apiKey, useDefaultCredentials); - - _root = new Lazy>(() => Client.GetRootAsync()); } + + public SeqApiClient Client { get; } public ApiKeysResourceGroup ApiKeys => new ApiKeysResourceGroup(this); @@ -60,27 +62,71 @@ public SeqConnection(string serverUrl, string apiKey = null, bool useDefaultCred public WorkspacesResourceGroup Workspaces => new WorkspacesResourceGroup(this); + public async Task EnsureConnected(TimeSpan timeout) + { + var started = DateTime.UtcNow; + // Fractional milliseconds are lost here, but that's fine. + var wait = TimeSpan.FromMilliseconds(Math.Min(100, timeout.TotalMilliseconds)); + var deadline = started.Add(timeout); + while (!await ConnectAsync(DateTime.UtcNow > deadline)) + { + await Task.Delay(wait); + } + } + + async Task ConnectAsync(bool throwOnFailure) + { + HttpStatusCode statusCode; + + try + { + statusCode = (await Client.HttpClient.GetAsync("api")).StatusCode; + } + catch + { + if (throwOnFailure) + throw; + + return false; + } + + if (statusCode == HttpStatusCode.OK) + return true; + + if (!throwOnFailure) + return false; + + throw new WebException($"Could not connect to the Seq API endpoint: {(int)statusCode}/{statusCode}."); + } + public Task LoadResourceGroupAsync(string name, CancellationToken cancellationToken = default) { - // Initially, we want to put an incomplete task into the cache so that any concurrent attempts to load the - // same resource group will wait on the same pending call. - var cached = _resourceGroups.GetOrAdd(name, s => ResourceGroupFactory(s, cancellationToken)); - - if (!cached.IsFaulted && !cached.IsCanceled) - return cached; - - // If the cached task failed (ideally a rare situation), clobber it and return a new task (less worried about - // overlapping/concurrent calls on this path). - return _resourceGroups.AddOrUpdate(name, - s => ResourceGroupFactory(s, cancellationToken), - (s, _) => ResourceGroupFactory(s, cancellationToken)); + lock (_sync) + { + if (_resourceGroups.TryGetValue(name, out var cached) && !cached.IsFaulted && !cached.IsCanceled) + return cached; + + var attempt = ResourceGroupFactory(name, cancellationToken); + _resourceGroups.Add(name, attempt); + return attempt; + } } async Task ResourceGroupFactory(string name, CancellationToken cancellationToken = default) { - return await Client.GetAsync(await _root.Value, name + "Resources", cancellationToken: cancellationToken).ConfigureAwait(false); - } + Task root; - public SeqApiClient Client { get; } + // Reentrant, see above + lock (_sync) + { + if (_root == null || _root.IsFaulted || _root.IsCanceled) + _root = Client.GetRootAsync(cancellationToken); + + root = _root; + } + + var rootEntity = await root.ConfigureAwait(false); + return await Client.GetAsync(rootEntity, name + "Resources", cancellationToken: cancellationToken).ConfigureAwait(false); + } } } From 2a78a53336911bf8ace14adc016d044872933115 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 15:21:00 +1000 Subject: [PATCH 05/10] More cached task logic cleanup --- src/Seq.Api/SeqConnection.cs | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Seq.Api/SeqConnection.cs b/src/Seq.Api/SeqConnection.cs index e0c17eb..d57803a 100644 --- a/src/Seq.Api/SeqConnection.cs +++ b/src/Seq.Api/SeqConnection.cs @@ -99,34 +99,36 @@ async Task ConnectAsync(bool throwOnFailure) throw new WebException($"Could not connect to the Seq API endpoint: {(int)statusCode}/{statusCode}."); } - public Task LoadResourceGroupAsync(string name, CancellationToken cancellationToken = default) + public async Task LoadResourceGroupAsync(string name, CancellationToken cancellationToken = default) { + Task loadRoot; lock (_sync) { - if (_resourceGroups.TryGetValue(name, out var cached) && !cached.IsFaulted && !cached.IsCanceled) - return cached; + if (_root == null || _root.IsFaulted || _root.IsCanceled) + _root = Client.GetRootAsync(cancellationToken); - var attempt = ResourceGroupFactory(name, cancellationToken); - _resourceGroups.Add(name, attempt); - return attempt; + loadRoot = _root; } - } - async Task ResourceGroupFactory(string name, CancellationToken cancellationToken = default) - { - Task root; + var rootEntity = await loadRoot.ConfigureAwait(false); - // Reentrant, see above + Task loadGroup; lock (_sync) { - if (_root == null || _root.IsFaulted || _root.IsCanceled) - _root = Client.GetRootAsync(cancellationToken); - - root = _root; + // ReSharper disable once InvertIf + if (!_resourceGroups.TryGetValue(name, out loadGroup) || loadGroup.IsFaulted || loadGroup.IsCanceled) + { + loadGroup = GetResourceGroup(rootEntity, name, cancellationToken); + _resourceGroups.Add(name, loadGroup); + } } - var rootEntity = await root.ConfigureAwait(false); - return await Client.GetAsync(rootEntity, name + "Resources", cancellationToken: cancellationToken).ConfigureAwait(false); + return await loadGroup.ConfigureAwait(false); + } + + async Task GetResourceGroup(RootEntity root, string name, CancellationToken cancellationToken = default) + { + return await Client.GetAsync(root, name + "Resources", cancellationToken: cancellationToken).ConfigureAwait(false); } } } From 3baf361aa287764731c5c74a5391f03d12af07be Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 15:56:28 +1000 Subject: [PATCH 06/10] Allow a timeout to be passed through SeqConnnection to the underlying HttpClient --- src/Seq.Api/Client/SeqApiClient.cs | 6 +- src/Seq.Api/SeqConnection.cs | 88 ++++++++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/Seq.Api/Client/SeqApiClient.cs b/src/Seq.Api/Client/SeqApiClient.cs index e59174f..e313df4 100644 --- a/src/Seq.Api/Client/SeqApiClient.cs +++ b/src/Seq.Api/Client/SeqApiClient.cs @@ -35,7 +35,7 @@ public class SeqApiClient : IDisposable Converters = { new StringEnumConverter(), new LinkCollectionConverter() } }); - public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCredentials = true) + public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCredentials = true, TimeSpan? requestTimeout = null) { ServerUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl)); @@ -53,6 +53,8 @@ public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCrede baseAddress += "/"; HttpClient = new HttpClient(handler) { BaseAddress = new Uri(baseAddress) }; + if (requestTimeout != null) + HttpClient.Timeout = requestTimeout.Value; } public string ServerUrl { get; } @@ -199,7 +201,7 @@ async Task HttpSendAsync(HttpRequestMessage request, CancellationToken c if (payload != null && payload.TryGetValue("Error", out var error) && error != null) throw new SeqApiException($"{(int)response.StatusCode} - {error}", response.StatusCode); - throw new SeqApiException($"The Seq request failed ({(int)response.StatusCode}).", response.StatusCode); + throw new SeqApiException($"The Seq request failed ({(int)response.StatusCode}/{response.StatusCode}).", response.StatusCode); } HttpContent MakeJsonContent(object content) diff --git a/src/Seq.Api/SeqConnection.cs b/src/Seq.Api/SeqConnection.cs index d57803a..cc5aa28 100644 --- a/src/Seq.Api/SeqConnection.cs +++ b/src/Seq.Api/SeqConnection.cs @@ -10,58 +10,136 @@ namespace Seq.Api { + /// + /// Exposes high-level (typed) interactions with the Seq API through various resource groups. + /// public class SeqConnection : ISeqConnection { readonly object _sync = new object(); readonly Dictionary> _resourceGroups = new Dictionary>(); Task _root; - public SeqConnection(string serverUrl, string apiKey = null, bool useDefaultCredentials = true) + /// + /// Construct a . + /// + /// The base URL of the Seq server. + /// An API key to use when making requests to the server, if required. + /// Whether default credentials will be sent with HTTP requests; the default is true. + /// The time to wait before canceling long-running HTTP requests; the default (null) is to use the + /// system request timeout, normally 100 seconds. To disable timing out at the client, pass . + public SeqConnection(string serverUrl, string apiKey = null, bool useDefaultCredentials = true, TimeSpan? requestTimeout = null) { if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl)); - Client = new SeqApiClient(serverUrl, apiKey, useDefaultCredentials); + Client = new SeqApiClient(serverUrl, apiKey, useDefaultCredentials, requestTimeout); } + /// + /// Access the lower-level that can be used for resource-oriented navigation through + /// the HTTP API. + /// public SeqApiClient Client { get; } + /// + /// Perform operations on API keys. + /// public ApiKeysResourceGroup ApiKeys => new ApiKeysResourceGroup(this); + /// + /// Perform operations on Seq app instances. + /// public AppInstancesResourceGroup AppInstances => new AppInstancesResourceGroup(this); + /// + /// Perform operations on Seq app packages. + /// public AppsResourceGroup Apps => new AppsResourceGroup(this); + /// + /// Perform operations on backups. + /// public BackupsResourceGroup Backups => new BackupsResourceGroup(this); + /// + /// Perform operations on dashboards. + /// public DashboardsResourceGroup Dashboards => new DashboardsResourceGroup(this); + /// + /// Execute SQL-style queries against the API. + /// public DataResourceGroup Data => new DataResourceGroup(this); + /// + /// Access server diagnostics. + /// public DiagnosticsResourceGroup Diagnostics => new DiagnosticsResourceGroup(this); + /// + /// Read and subscribe to events from the event store. + /// public EventsResourceGroup Events => new EventsResourceGroup(this); + /// + /// Perform operations on queries and filter expressions. + /// public ExpressionsResourceGroup Expressions => new ExpressionsResourceGroup(this); + /// + /// Perform operations on NuGet feeds. + /// public FeedsResourceGroup Feeds => new FeedsResourceGroup(this); + /// + /// Perform operations on the Seq license certificate. + /// public LicensesResourceGroup Licenses => new LicensesResourceGroup(this); + /// + /// Perform operations on permalinked events. + /// public PermalinksResourceGroup Permalinks => new PermalinksResourceGroup(this); + /// + /// Perform operations on retention policies. + /// public RetentionPoliciesResourceGroup RetentionPolicies => new RetentionPoliciesResourceGroup(this); + /// + /// Perform operations on system settings. + /// public SettingsResourceGroup Settings => new SettingsResourceGroup(this); + /// + /// Perform operations on signals. + /// public SignalsResourceGroup Signals => new SignalsResourceGroup(this); + /// + /// Perform operations on saved SQL queries. + /// public SqlQueriesResourceGroup SqlQueries => new SqlQueriesResourceGroup(this); + /// + /// Perform operations on known available Seq versions. + /// public UpdatesResourceGroup Updates => new UpdatesResourceGroup(this); + /// + /// Perform operations on users. + /// public UsersResourceGroup Users => new UsersResourceGroup(this); + /// + /// Perform operations on workspaces. + /// public WorkspacesResourceGroup Workspaces => new WorkspacesResourceGroup(this); + /// + /// Check that the Seq API is available. If the initial attempt fails (i.e. the server is starting up), + /// the method will try every 100 milliseconds until is reached. + /// + /// The maximum amount of time to retry until giving up. + /// A task that will complete if the API could be reached, or fault otherwise. public async Task EnsureConnected(TimeSpan timeout) { var started = DateTime.UtcNow; @@ -96,10 +174,10 @@ async Task ConnectAsync(bool throwOnFailure) if (!throwOnFailure) return false; - throw new WebException($"Could not connect to the Seq API endpoint: {(int)statusCode}/{statusCode}."); + throw new SeqApiException($"Could not connect to the Seq API endpoint: {(int)statusCode}/{statusCode}.", statusCode); } - public async Task LoadResourceGroupAsync(string name, CancellationToken cancellationToken = default) + async Task ISeqConnection.LoadResourceGroupAsync(string name, CancellationToken cancellationToken) { Task loadRoot; lock (_sync) @@ -126,7 +204,7 @@ public async Task LoadResourceGroupAsync(string name, Cancellatio return await loadGroup.ConfigureAwait(false); } - async Task GetResourceGroup(RootEntity root, string name, CancellationToken cancellationToken = default) + async Task GetResourceGroup(RootEntity root, string name, CancellationToken cancellationToken) { return await Client.GetAsync(root, name + "Resources", cancellationToken: cancellationToken).ConfigureAwait(false); } From f1aab15c18191ec0dee4a82756556824649ee4be Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 20:11:18 +1000 Subject: [PATCH 07/10] Document SeqApiClient; reorganize so that the exposed HttpClient sends authenticated requests with correct Accept headers --- src/Seq.Api/Client/SeqApiClient.cs | 167 ++++++++++++++++++++++++++--- 1 file changed, 155 insertions(+), 12 deletions(-) diff --git a/src/Seq.Api/Client/SeqApiClient.cs b/src/Seq.Api/Client/SeqApiClient.cs index e313df4..2187c74 100644 --- a/src/Seq.Api/Client/SeqApiClient.cs +++ b/src/Seq.Api/Client/SeqApiClient.cs @@ -19,6 +19,9 @@ namespace Seq.Api.Client { + /// + /// A low-level client that provides navigation over the linked resource structure of the Seq HTTP API. + /// public class SeqApiClient : IDisposable { readonly string _apiKey; @@ -35,6 +38,14 @@ public class SeqApiClient : IDisposable Converters = { new StringEnumConverter(), new LinkCollectionConverter() } }); + /// + /// Construct a . + /// + /// The base URL of the Seq server. + /// An API key to use when making requests to the server, if required. + /// Whether default credentials will be sent with HTTP requests; the default is true. + /// The time to wait before canceling long-running HTTP requests; the default (null) is to use the + /// system request timeout, normally 100 seconds. To disable timing out at the client, pass . public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCredentials = true, TimeSpan? requestTimeout = null) { ServerUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl)); @@ -53,45 +64,110 @@ public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCrede baseAddress += "/"; HttpClient = new HttpClient(handler) { BaseAddress = new Uri(baseAddress) }; + HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(SeqApiV7MediaType)); + + if (_apiKey != null) + HttpClient.DefaultRequestHeaders.Add("X-Seq-ApiKey", _apiKey); + if (requestTimeout != null) HttpClient.Timeout = requestTimeout.Value; } + /// + /// The base URL of the Seq server. + /// public string ServerUrl { get; } + /// + /// The HTTP client used when making requests to the Seq server. The HTTP client is configured with the server's base address, collects any cookies + /// sent with responses from the API, and will send the appropriate Accept and X-Seq-ApiKey headers by default. + /// public HttpClient HttpClient { get; } + /// + /// Get the root entity describing the server and providing links to other resources available from the API. + /// + /// A supporting cancellation. + /// The root entity. public Task GetRootAsync(CancellationToken cancellationToken = default) { return HttpGetAsync("api", cancellationToken); } + /// + /// Issue a GET request returning a by following the from . + /// + /// The entity type into which the response should be deserialized. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// A supporting cancellation. + /// The response entity. public Task GetAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); return HttpGetAsync(linkUri, cancellationToken); } + /// + /// Issue a GET request returning a string by following the from . + /// + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// A supporting cancellation. + /// The response as a string. public Task GetStringAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); return HttpGetStringAsync(linkUri, cancellationToken); } + /// + /// Issue a GET request returning a list of by following the from . + /// + /// The entity type into which the response items should be deserialized. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// A supporting cancellation. + /// The response entities. public Task> ListAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); return HttpGetAsync>(linkUri, cancellationToken); } + /// + /// Issue a POST request accepting a serialized to a from . + /// + /// The entity type that will be serialized into the request payload. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// A task that signals request completion. public async Task PostAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); var request = new HttpRequestMessage(HttpMethod.Post, linkUri) { Content = MakeJsonContent(content) }; var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); - new StreamReader(stream).ReadToEnd(); + using (var reader = new StreamReader(stream)) + reader.ReadToEnd(); } + /// + /// Issue a POST request accepting a serialized and returning a by following from . + /// + /// The entity type that will be serialized into the request payload. + /// The entity type into which the response should be deserialized. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// The response entity. public async Task PostAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); @@ -100,14 +176,35 @@ public async Task PostAsync(ILinked entity, strin return _serializer.Deserialize(new JsonTextReader(new StreamReader(stream))); } + /// + /// Issue a POST request accepting a serialized and returning a string by following from . + /// + /// The entity type that will be serialized into the request payload. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// The response string. public async Task PostReadStringAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); var request = new HttpRequestMessage(HttpMethod.Post, linkUri) { Content = MakeJsonContent(content) }; var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); - return await new StreamReader(stream).ReadToEndAsync(); + using (var reader = new StreamReader(stream)) + return await reader.ReadToEndAsync(); } + /// + /// Issue a POST request accepting a serialized and returning a stream by following from . + /// + /// The entity type that will be serialized into the request payload. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// The response stream. public async Task PostReadStreamAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); @@ -115,22 +212,55 @@ public async Task PostReadStreamAsync(ILinked entity, string li return await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); } + /// + /// Issue a PUT request accepting a serialized to a from . + /// + /// The entity type that will be serialized into the request payload. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// A task that signals request completion. public async Task PutAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); var request = new HttpRequestMessage(HttpMethod.Put, linkUri) { Content = MakeJsonContent(content) }; var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); - new StreamReader(stream).ReadToEnd(); + using (var reader = new StreamReader(stream)) + reader.ReadToEnd(); } + /// + /// Issue a DELETE request accepting a serialized to a from . + /// + /// The entity type that will be serialized into the request payload. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// A task that signals request completion. public async Task DeleteAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); var request = new HttpRequestMessage(HttpMethod.Delete, linkUri) { Content = MakeJsonContent(content) }; var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); - new StreamReader(stream).ReadToEnd(); + using (var reader = new StreamReader(stream)) + reader.ReadToEnd(); } + /// + /// Issue a DELETE request accepting a serialized to a from . + /// + /// The entity type that will be serialized into the request payload. + /// The entity type into which the response should be deserialized. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// The request content. + /// A supporting cancellation. + /// The response entity. public async Task DeleteAsync(ILinked entity, string link, TEntity content, IDictionary parameters = null, CancellationToken cancellationToken = default) { var linkUri = ResolveLink(entity, link, parameters); @@ -139,11 +269,28 @@ public async Task DeleteAsync(ILinked entity, str return _serializer.Deserialize(new JsonTextReader(new StreamReader(stream))); } + /// + /// Connect to a websocket at the address specified by following from . + /// + /// The type of the values received over the websocket. + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// A supporting cancellation. + /// A stream of values from the websocket. public async Task> StreamAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default) { return await WebSocketStreamAsync(entity, link, parameters, reader => _serializer.Deserialize(new JsonTextReader(reader)), cancellationToken); } + /// + /// Connect to a websocket at the address specified by following from . + /// + /// An entity previously retrieved from the API. + /// The name of the outbound link template present in 's collection. + /// Named parameters to substitute into the link template, if required. + /// A supporting cancellation. + /// A stream of raw messages from the websocket. public async Task> StreamTextAsync(ILinked entity, string link, IDictionary parameters = null, CancellationToken cancellationToken = default) { return await WebSocketStreamAsync(entity, link, parameters, reader => reader.ReadToEnd(), cancellationToken); @@ -174,16 +321,12 @@ async Task HttpGetStringAsync(string url, CancellationToken cancellation { var request = new HttpRequestMessage(HttpMethod.Get, url); var stream = await HttpSendAsync(request, cancellationToken).ConfigureAwait(false); - return await new StreamReader(stream).ReadToEndAsync(); + using (var reader = new StreamReader(stream)) + return await reader.ReadToEndAsync(); } async Task HttpSendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - if (_apiKey != null) - request.Headers.Add("X-Seq-ApiKey", _apiKey); - - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(SeqApiV7MediaType)); - var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); @@ -213,8 +356,7 @@ HttpContent MakeJsonContent(object content) static string ResolveLink(ILinked entity, string link, IDictionary parameters = null) { - Link linkItem; - if (!entity.Links.TryGetValue(link, out linkItem)) + if (!entity.Links.TryGetValue(link, out var linkItem)) throw new NotSupportedException($"The requested link `{link}` isn't available on entity `{entity}`."); var expression = linkItem.GetUri(); @@ -238,6 +380,7 @@ static string ResolveLink(ILinked entity, string link, IDictionary> public void Dispose() { HttpClient.Dispose(); From ef9b2594da0de66f123c09466f0d6be0d74d73f1 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 21:20:56 +1000 Subject: [PATCH 08/10] Improve documentation coverage of core API client/model types. Fixes some historical issues with Link, removing the confusing isLiteral mechanism to cement the expectation that all links in the Seq API are encoded as URI templates, even if they contain zero parameters --- seq-api.sln.DotSettings | 5 + src/Seq.Api/Client/SeqApiClient.cs | 36 ++++--- src/Seq.Api/Client/SeqApiException.cs | 16 +++- src/Seq.Api/Model/Entity.cs | 30 +++++- src/Seq.Api/Model/ILinked.cs | 22 ++++- src/Seq.Api/Model/Link.cs | 93 ++++++++++++------- src/Seq.Api/Model/LinkCollection.cs | 24 +++-- src/Seq.Api/Model/ResourceGroup.cs | 24 ++++- src/Seq.Api/SeqConnection.cs | 16 +++- .../Serialization/LinkCollectionConverter.cs | 20 +++- .../Streams/ObservableStream.Unsubscriber.cs | 7 +- src/Seq.Api/Streams/ObservableStream.cs | 8 +- 12 files changed, 230 insertions(+), 71 deletions(-) diff --git a/seq-api.sln.DotSettings b/seq-api.sln.DotSettings index 969cc30..6e98e3d 100644 --- a/seq-api.sln.DotSettings +++ b/seq-api.sln.DotSettings @@ -1,11 +1,16 @@  AD True + True True + True + True True True True True + True True + True True diff --git a/src/Seq.Api/Client/SeqApiClient.cs b/src/Seq.Api/Client/SeqApiClient.cs index 2187c74..6de445c 100644 --- a/src/Seq.Api/Client/SeqApiClient.cs +++ b/src/Seq.Api/Client/SeqApiClient.cs @@ -1,4 +1,18 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -359,25 +373,7 @@ static string ResolveLink(ILinked entity, string link, IDictionary p.Key).Except(template.GetParameterNames()).ToArray(); - if (missing.Any()) - throw new ArgumentException($"The URI template `{expression}` does not contain parameter: `{string.Join("`, `", missing)}`."); - - foreach (var parameter in parameters) - { - var value = parameter.Value is DateTime time - ? time.ToString("O") - : parameter.Value; - - template.SetParameter(parameter.Key, value); - } - } - - return template.Resolve(); + return linkItem.GetUri(parameters); } /// > diff --git a/src/Seq.Api/Client/SeqApiException.cs b/src/Seq.Api/Client/SeqApiException.cs index 71d5966..fc53da9 100644 --- a/src/Seq.Api/Client/SeqApiException.cs +++ b/src/Seq.Api/Client/SeqApiException.cs @@ -1,4 +1,18 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Net; namespace Seq.Api.Client diff --git a/src/Seq.Api/Model/Entity.cs b/src/Seq.Api/Model/Entity.cs index b31f7c3..7d3a98f 100644 --- a/src/Seq.Api/Model/Entity.cs +++ b/src/Seq.Api/Model/Entity.cs @@ -1,14 +1,42 @@ -namespace Seq.Api.Model +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Seq.Api.Model { + /// + /// A uniquely-identifiable resource available from the Seq HTTP API. + /// public abstract class Entity : ILinked { + /// + /// Construct an . + /// protected Entity() { Links = new LinkCollection(); } + /// + /// The entity's unique identifier. This will be null if a newly-instantiated + /// has not yet been sent to the server. + /// public string Id { get; set; } + /// + /// A collection of outbound links from the entity. This will be empty if the entity + /// was instantiated locally and not received from the API. + /// public LinkCollection Links { get; set; } } } diff --git a/src/Seq.Api/Model/ILinked.cs b/src/Seq.Api/Model/ILinked.cs index df7dc62..ae30187 100644 --- a/src/Seq.Api/Model/ILinked.cs +++ b/src/Seq.Api/Model/ILinked.cs @@ -1,7 +1,27 @@ -namespace Seq.Api.Model +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Seq.Api.Model { + /// + /// A resource that carries outbound links. + /// public interface ILinked { + /// + /// Links associated with the resource. + /// LinkCollection Links { get; } } } \ No newline at end of file diff --git a/src/Seq.Api/Model/Link.cs b/src/Seq.Api/Model/Link.cs index b3a77d2..0513d68 100644 --- a/src/Seq.Api/Model/Link.cs +++ b/src/Seq.Api/Model/Link.cs @@ -1,50 +1,81 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using Tavis.UriTemplates; +// ReSharper disable PossibleMultipleEnumeration + namespace Seq.Api.Model { - public class Link + /// + /// A hypermedia link. A link is an RFC 6570 URI template that can be + /// parameterized in order to produce a complete URI (if the template contains no + /// parameters then it may also be a literal URI). + /// + public struct Link { - readonly bool _isLiteral; - readonly string _href; - readonly IDictionary _parameters; - - public Link(string href, bool isLiteral = true) - : this(href, new Dictionary()) - { - _isLiteral = isLiteral; - } + /// + /// An empty link. + /// + public static Link Empty { get; } = default; - public Link(string href, IDictionary parameters) + /// + /// Construct a link. + /// + /// The URI template. + public Link(string template) { - if (href == null) throw new ArgumentNullException(nameof(href)); - if (parameters == null) throw new ArgumentNullException(nameof(parameters)); - _href = href; - _parameters = parameters; + Template = template ?? throw new ArgumentNullException(nameof(template)); } - public Link(string href, object parameters) - : this(href, parameters - .GetType() - .GetTypeInfo() - .DeclaredProperties - .ToDictionary(p => p.Name, p => p.GetValue(parameters))) - { - } + /// + /// Get the unprocessed URI template. + /// + public string Template { get; } - public string GetUri() + /// + /// Parameterize the link to construct a URI. + /// + /// Parameters to substitute into the template, if any. + /// A constructed URI. + /// This method ensures that templates containing parameters cannot be accidentally + /// used as URIs. + public string GetUri(IDictionary parameters = null) { - if (_isLiteral) return _href; + if (Template == null) throw new InvalidOperationException("Attempted to process an empty URI template."); - var template = new UriTemplate(_href); - foreach (var parameter in _parameters) + var template = new UriTemplate(Template); + if (parameters != null) { - template.SetParameter(parameter.Key, parameter.Value); + var missing = parameters.Select(p => p.Key).Except(template.GetParameterNames()); + if (missing.Any()) + throw new ArgumentException($"The URI template `{Template}` does not contain parameter: `{string.Join("`, `", missing)}`."); + + foreach (var parameter in parameters) + { + var value = parameter.Value is DateTime time + ? time.ToString("O") + : parameter.Value; + + template.SetParameter(parameter.Key, value); + } } + return template.Resolve(); } } -} \ No newline at end of file +} diff --git a/src/Seq.Api/Model/LinkCollection.cs b/src/Seq.Api/Model/LinkCollection.cs index e4afbf3..aebab05 100644 --- a/src/Seq.Api/Model/LinkCollection.cs +++ b/src/Seq.Api/Model/LinkCollection.cs @@ -1,18 +1,30 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; using Newtonsoft.Json; using Seq.Api.Serialization; namespace Seq.Api.Model { + /// + /// A collection of s indexed by case-insensitive name. + /// [JsonConverter(typeof(LinkCollectionConverter))] public class LinkCollection : Dictionary { public LinkCollection() : base(StringComparer.OrdinalIgnoreCase) { } - - public string GetUri(string name) - { - return this[name].GetUri(); - } } } diff --git a/src/Seq.Api/Model/ResourceGroup.cs b/src/Seq.Api/Model/ResourceGroup.cs index f634ddd..3837ca8 100644 --- a/src/Seq.Api/Model/ResourceGroup.cs +++ b/src/Seq.Api/Model/ResourceGroup.cs @@ -1,12 +1,34 @@ -namespace Seq.Api.Model +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Seq.Api.Model { + /// + /// A resource group is a logical partitioning of the API, usually + /// associated with a particular type of entity. + /// public class ResourceGroup : ILinked { + /// + /// Construct a . + /// public ResourceGroup() { Links = new LinkCollection(); } + /// public LinkCollection Links { get; set; } } } diff --git a/src/Seq.Api/SeqConnection.cs b/src/Seq.Api/SeqConnection.cs index cc5aa28..3c03704 100644 --- a/src/Seq.Api/SeqConnection.cs +++ b/src/Seq.Api/SeqConnection.cs @@ -1,4 +1,18 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; using System.Net; using System.Threading; diff --git a/src/Seq.Api/Serialization/LinkCollectionConverter.cs b/src/Seq.Api/Serialization/LinkCollectionConverter.cs index 01dc797..c560bed 100644 --- a/src/Seq.Api/Serialization/LinkCollectionConverter.cs +++ b/src/Seq.Api/Serialization/LinkCollectionConverter.cs @@ -1,4 +1,18 @@ -using System; +// Copyright 2014-2019 Datalust and contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; @@ -6,12 +20,12 @@ namespace Seq.Api.Serialization { - public class LinkCollectionConverter : JsonConverter + class LinkCollectionConverter : JsonConverter { public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { var lc = (LinkCollection)value; - var dictionary = lc.ToDictionary(kv => kv.Key, kv => kv.Value.GetUri()); + var dictionary = lc.ToDictionary(kv => kv.Key, kv => kv.Value.Template); serializer.Serialize(writer, dictionary); } diff --git a/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs b/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs index e4380e3..f7f510b 100644 --- a/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs +++ b/src/Seq.Api/Streams/ObservableStream.Unsubscriber.cs @@ -12,6 +12,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + using System; namespace Seq.Api.Streams @@ -25,10 +26,8 @@ sealed class Unsubscriber : IDisposable public Unsubscriber(ObservableStream sink, IObserver observer) { - if (sink == null) throw new ArgumentNullException(nameof(sink)); - if (observer == null) throw new ArgumentNullException(nameof(observer)); - _stream = sink; - _observer = observer; + _stream = sink ?? throw new ArgumentNullException(nameof(sink)); + _observer = observer ?? throw new ArgumentNullException(nameof(observer)); } public void Dispose() diff --git a/src/Seq.Api/Streams/ObservableStream.cs b/src/Seq.Api/Streams/ObservableStream.cs index 0ddc419..2af4853 100644 --- a/src/Seq.Api/Streams/ObservableStream.cs +++ b/src/Seq.Api/Streams/ObservableStream.cs @@ -24,8 +24,10 @@ namespace Seq.Api.Streams { - // Some questionable synchronization in this one; probably better to take the Rx dependency - // and use the Rx support classes here. + /// + /// A stream of values read from a . + /// + /// The type of the values. public sealed partial class ObservableStream : IObservable, IDisposable { readonly object _syncRoot = new object(); @@ -42,6 +44,7 @@ internal ObservableStream(ClientWebSocket socket, Func deserializ _socket = socket ?? throw new ArgumentNullException(nameof(socket)); } + /// public IDisposable Subscribe(IObserver observer) { if (observer == null) throw new ArgumentNullException(nameof(observer)); @@ -182,6 +185,7 @@ static void OnError(IEnumerable exceptions) throw new AggregateException("At least one observer failed to accept the event", exceptions); } + /// public void Dispose() { lock (_syncRoot) From de522651a59103133671e5f0b1fe29c33098e5e8 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Thu, 12 Sep 2019 21:27:16 +1000 Subject: [PATCH 09/10] Add some very basic tests for Link --- test/Seq.Api.Tests/LinkTests.cs | 36 +++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 test/Seq.Api.Tests/LinkTests.cs diff --git a/test/Seq.Api.Tests/LinkTests.cs b/test/Seq.Api.Tests/LinkTests.cs new file mode 100644 index 0000000..ae6a636 --- /dev/null +++ b/test/Seq.Api.Tests/LinkTests.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Seq.Api.Model; +using Xunit; + +namespace Seq.Api.Tests +{ + public class LinkTests + { + [Fact] + public void ALinkWithNoParametersIsLiteral() + { + const string uri = "https://example.com"; + var link = new Link(uri); + var constructed = link.GetUri(); + Assert.Equal(uri, constructed); + } + + [Fact] + public void AParameterizedLinkCanBeConstructed() + { + const string template = "https://example.com/{name}"; + var link = new Link(template); + var constructed = link.GetUri(new Dictionary {["name"] = "test"}); + Assert.Equal("https://example.com/test", constructed); + } + + [Fact] + public void InvalidParametersAreDetected() + { + const string template = "https://example.com"; + var link = new Link(template); + Assert.Throws(() => link.GetUri(new Dictionary {["name"] = "test"})); + } + } +} From 379f2227a1adf247d6779caa5ab2b95a048f4dfa Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Fri, 13 Sep 2019 14:02:08 +1000 Subject: [PATCH 10/10] Partial revert of #69 - Timeout is already accessible via SeqConnection.Client.HttpClient.Timeout - no need for breaking ctor change --- src/Seq.Api/Client/SeqApiClient.cs | 9 +-------- src/Seq.Api/SeqConnection.cs | 6 ++---- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Seq.Api/Client/SeqApiClient.cs b/src/Seq.Api/Client/SeqApiClient.cs index 6de445c..f505450 100644 --- a/src/Seq.Api/Client/SeqApiClient.cs +++ b/src/Seq.Api/Client/SeqApiClient.cs @@ -15,7 +15,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -26,7 +25,6 @@ using Seq.Api.Model; using Seq.Api.Model.Root; using Seq.Api.Serialization; -using Tavis.UriTemplates; using System.Threading; using Seq.Api.Streams; using System.Net.WebSockets; @@ -58,9 +56,7 @@ public class SeqApiClient : IDisposable /// The base URL of the Seq server. /// An API key to use when making requests to the server, if required. /// Whether default credentials will be sent with HTTP requests; the default is true. - /// The time to wait before canceling long-running HTTP requests; the default (null) is to use the - /// system request timeout, normally 100 seconds. To disable timing out at the client, pass . - public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCredentials = true, TimeSpan? requestTimeout = null) + public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCredentials = true) { ServerUrl = serverUrl ?? throw new ArgumentNullException(nameof(serverUrl)); @@ -82,9 +78,6 @@ public SeqApiClient(string serverUrl, string apiKey = null, bool useDefaultCrede if (_apiKey != null) HttpClient.DefaultRequestHeaders.Add("X-Seq-ApiKey", _apiKey); - - if (requestTimeout != null) - HttpClient.Timeout = requestTimeout.Value; } /// diff --git a/src/Seq.Api/SeqConnection.cs b/src/Seq.Api/SeqConnection.cs index 3c03704..900968b 100644 --- a/src/Seq.Api/SeqConnection.cs +++ b/src/Seq.Api/SeqConnection.cs @@ -39,12 +39,10 @@ public class SeqConnection : ISeqConnection /// The base URL of the Seq server. /// An API key to use when making requests to the server, if required. /// Whether default credentials will be sent with HTTP requests; the default is true. - /// The time to wait before canceling long-running HTTP requests; the default (null) is to use the - /// system request timeout, normally 100 seconds. To disable timing out at the client, pass . - public SeqConnection(string serverUrl, string apiKey = null, bool useDefaultCredentials = true, TimeSpan? requestTimeout = null) + public SeqConnection(string serverUrl, string apiKey = null, bool useDefaultCredentials = true) { if (serverUrl == null) throw new ArgumentNullException(nameof(serverUrl)); - Client = new SeqApiClient(serverUrl, apiKey, useDefaultCredentials, requestTimeout); + Client = new SeqApiClient(serverUrl, apiKey, useDefaultCredentials); } ///