From 11160dabf9e94e9161dbd934fc4e124b7b5f61e4 Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Mon, 28 Jul 2025 13:12:16 +0200 Subject: [PATCH 1/8] v2/feature/[Add Validation Support for Cortex Mediator] #129: Add new library to enrich the functionality of Cortex.Mediator - Cortex.Mediator.Behaviors.FluentValidation Add ValidationCommandBehavior Add ValidationQueryBehavior Update README.md file --- Cortex.sln | 6 + README.md | 3 + .../Assets/andyX.png | Bin 0 -> 63537 bytes .../Assets/license.md | 20 ++ ...Mediator.Behaviors.FluentValidation.csproj | 72 +++++++ .../README.md | 181 ++++++++++++++++++ .../ValidationCommandBehavior.cs | 43 +++++ .../ValidationQueryBehavior.cs | 40 ++++ 8 files changed, 365 insertions(+) create mode 100644 src/Cortex.Mediator.Behaviors.FluentValidation/Assets/andyX.png create mode 100644 src/Cortex.Mediator.Behaviors.FluentValidation/Assets/license.md create mode 100644 src/Cortex.Mediator.Behaviors.FluentValidation/Cortex.Mediator.Behaviors.FluentValidation.csproj create mode 100644 src/Cortex.Mediator.Behaviors.FluentValidation/README.md create mode 100644 src/Cortex.Mediator.Behaviors.FluentValidation/ValidationCommandBehavior.cs create mode 100644 src/Cortex.Mediator.Behaviors.FluentValidation/ValidationQueryBehavior.cs diff --git a/Cortex.sln b/Cortex.sln index 025b0b9..7efbb3f 100644 --- a/Cortex.sln +++ b/Cortex.sln @@ -57,6 +57,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Streams.Elasticsearc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Types", "src\Cortex.Types\Cortex.Types.csproj", "{64E12D4C-FBB2-4004-8316-C886CBFC614B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Mediator.Behaviors.FluentValidation", "src\Cortex.Mediator.Behaviors.FluentValidation\Cortex.Mediator.Behaviors.FluentValidation.csproj", "{44A166BD-01E9-4A4B-9BC5-7DE01B472E73}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -170,6 +172,10 @@ Global {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Debug|Any CPU.Build.0 = Debug|Any CPU {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Release|Any CPU.ActiveCfg = Release|Any CPU {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Release|Any CPU.Build.0 = Release|Any CPU + {44A166BD-01E9-4A4B-9BC5-7DE01B472E73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44A166BD-01E9-4A4B-9BC5-7DE01B472E73}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44A166BD-01E9-4A4B-9BC5-7DE01B472E73}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44A166BD-01E9-4A4B-9BC5-7DE01B472E73}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 76df3ba..b4f342b 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,9 @@ - **Cortex.Mediator:** implementation of the Mediator pattern for .NET applications, designed to power clean, modular architectures like **Vertical Slice Architecture** and **CQRS**. [![NuGet Version](https://img.shields.io/nuget/v/Cortex.Mediator?label=Cortex.Mediator)](https://www.nuget.org/packages/Cortex.Mediator) +- **Cortex.Mediator.Behaviors.FluentValidation:** implementation of the FluentValidation validation for Commands and Queries +[![NuGet Version](https://img.shields.io/nuget/v/Cortex.Mediator.Behaviors.FluentValidation?label=Cortex.Mediator.Behaviors.FluentValidation)](https://www.nuget.org/packages/Cortex.Mediator.Behaviors.FluentValidation) + ## Getting Started diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/Assets/andyX.png b/src/Cortex.Mediator.Behaviors.FluentValidation/Assets/andyX.png new file mode 100644 index 0000000000000000000000000000000000000000..101a1fb10887915ba6cd81f7493120090cfab590 GIT binary patch literal 63537 zcmZ^K1yG#bvhCn5gAN`fxVuAOu!La2-5~@B?gV#t2yO}PK@)6nhu{)CxCHmSkN=!= z?|D`4?J9}^s&{vI@Ee~(vP<9_oU@vo z6sT&9d=K~m(dyOPS0GSr9L9qQ67VycgS?J22*lm_`~}BouDb*RZGse_uhcyZ4<8ri z*qTxp!tHumUMplBZ@UQnQov(KAb7d0l9Hh@q;8cGW-rlJuGKM*)2LPLbxJic6L+Os zwa;8_ali6m!SIuVmZh7kQi@t?a%nF{fFfn5UI4CGQoX7WO zg={rqe%>1auBYq776dj~yXg!L41FHi0a!?3p}w^FzAPW5fdM<4$Ef?|3cqxD#>Wec zLrS+qfA^G$-|TbCPo&S|cmFa}sHsC?{w2+4wN$1?tmknbz=SB=>qcaoS>kjyJ~_!a z^U)d@^9IRy7O|gV#}4O_g|@dcrW6LlRLp)(0HcN?IdMJyNrjYF&WgzPb9Ct0o8puq z>8<^L)Y?+DAf`{Tq`>4CZRhDoiq9ekoh>+D%neMw|7Tuyr*Mv9{G^>BV@YwcRQdwy zzb1>*W-<8<|2Xw18Hp(Ex#t*R%MXnhT$DMteHOBw7lGgxkJ!58=jH!1Og`~;+x_b= zAkuaIXLosRK`{v`5vzcZ2t_3w6u7c!vB0GDU<)7&q9$30yZ^0zRmO}$n=#- zi#T!b?rS%bE1u^!Y2tz^ntT=?Tfm8fH4r#mg4DKn1lD8Y?b1_eCgwjW5*_!)zN`Dk z;*~B4Nyr}8)JRGQAP;eJ0jU-7Fsx(8+bN1ei=^wc7bn_fU;p|CIL|FSl}N@gdA`ex zwvRO17;#DAiKF_#dXkM?rmVN*bvV~%{r?1|EsB7tH=JvShu*B`>Ncr`mQ~q(ziV+I&|1&O$0k;uzK3^H9{@Zck_9ZU!<*vR2yNdid@-Io_=w>D z?I5ll3v{}^x%$wtyA|*lPxlT=_J-;Xv8*^<|AYZtxltOdC*PPq0s`rO8mD?nFz`Jz zg1fsE?;J4aX3hnIXF2Am z*w?Q+cAqV5_>&=MTLGF`l_dG z5W~N}=l(v1Df?`$POL-nvF&sP8W3cfee|KNll4{9%b0aBpA0{pNZiscW#63qYYytk z8DNgy?hx(yM_$_|hpXM)wyU7)zeiylKDl9^zuMyp%?1zG*-pjGsxYl=p0|6;Nim;e@tq&?wE;Z`N7TH$wI?svV8zxvPwhM!pyrWsz)1d|{Z4cAc? z@M%jme>3-9@b!C`A({bvz@(k}0flDvN+D?v*oik9`Y$+K92h+s`Q2^_{56n_rXJki zqoM^xYF70;S<~Lc8m~iV~;yB;Oj%eJXC-S6_ zOnUvdjjTzBGwm;Q&+*Zk66^rpQ+M0bu`iNaj`>)+d(OZ9B7)@Ya?!~_d4K0eNIf%e z740CsA(rn#UBIAI?)$1i#tBUz$o97WA6Y~bVmxBDxD1}`g%wWz&kMf-MdIo2l!HUFYs21SEHnRuqFrLjVw|Z`w}rzOwY)~smi+Z*+9?Sc=&)IzqLoWZ z=$QFrv>TO)27&EwGlowm`0jtZ{&Gn*Q~&AX$M*?+|2<@P&)>rBVgb5#fnUn{3_la4YC9kFx(hq;Nio!^3q1lo4o2PPD?$OHM=Ms|e+lu-sjE&Ave4sD{|3k6r8#eU1`x}S{9#9QwpC+ke>Z}MCU4Gof- zsJ5+;$VneO+|F8A7oF%OWi>3}E%B+@Rq{EEqLOf*JBf|ARkTFDl4Ef~U$@zf9%D&nKOW)n9deuQyB3(k=(QP@u~5tD9(3U#T6?PPZAx*u_bZ5ZmjI$mYb93H#K z1c^eQonTA8NiKLV&$%^4A$&_GFygnkr7J&HE8`$d2)b%Kwe`94yBHx}(a6df# za*-6W)Y54)umW)4?p4qhcydt(>D*dqT3t@`h7-M14`=2x&lCAm%keO~Ao7Xf#UjC< z&;GVLVcR-bUBM!RQ+K%!%V|$boVCCVX<(P-sOxQf?sVgQX_3!|71$Kvv2Smo*R^qe?A(52ONzlBm$kAUBb;2_Z<)U4DgT%T3Y!TR z;*TlCY;~#>G2`NlIGwC5X{)(mC(b{TxH)Fc7G+5LddO*((fZOYjdQsQJ*Wc2Z5kGU z;^4LNmU)41Q;x+M2qNiQbX0?fE>awhao`*+huD0^#v@`Ey39qzK>w(*=1_-sV^Q4p z*4F7{U0M#cAI(EHH>#L=%uy`lEz=a|d3MiHo5FWhltg0V)up+ye!@s6zj13z^97T^ zjOhVgZ)k4D!VEfXOC0n~Hq7dRe0o~P*Mr3Z*!>(R^F4Bv>nUsH`IGn9WZRiM-czq* zlYGguvpEDRVHvK^Tz~tW#>Ar{9J3-h^EqqCSl=*nM$IPbaODtHTOsZV9&q3B-;>9Lcd&v8csLw*EP1hN-0e}GPNVsw8VU1+f5 zkF#XK_UHMSCMgX&R9QVaEf>dN&eBYOVfr{T5^f15JjQjoEs4&z){1K9L1ig7KR~0S zrs0Vchzd*Dd9e(1NDKKL=fy?`p@*SP?fpA9U;1y zbi;@0jnHEh2a9a^FjfMT!tHWzx+xb8eHO#6{q$ZIY9JS)7hHwFtOadYMO6mafz@2I zj@F|@mmIc7`lCUuJET)#+yv0bG(E?7MVK4>kLXP3CTd%AgF_VRP$7SjALscAXoX!Td3D8JXa$!t!x6<&eSFK&=EU*W( zzF^dD$ZA70=)@x69bsO+!qMM9O#)MfLEaDpgaLx6iugn2N{a^tNlZVWOgd#!(92$@ zM$&Krb*KhICQ^#nwhKN~hc#=3QZm=mm2E_(c?JnsaP&*6dn>Xv0_x#4!k7ykFP5(m z$&hzGeqX61Dh&(4AZj&Y%oKyqebi>#+V@P4dhXs4%jQ0RLtt^x%-dJ7;Kg*?YdJ#W zN7cx)UT8!!$jbm^vKJnI+we>n^^}hO>mM0a-42eGZf~TF{-$haFfc8TWFvN z;0Jzh(uk&XX|ZHg*>#hhJEvwfK5m4Vj+xadq$fF8ap+6Ko+J&P>aVbnV$s#X^+W`d z&I|aqs97A_6y)|eJjBqp%t<@3?(U+-AFs6MPn%Jl@$98xT*3R2mPc>Uvj|IQBC-1Z zkpX2{O;BdChoTsSMl zJ3-?Ir7)b>x1xg1&8rL)9g@O0>n(V&(10ugTb5SHHe+GwsH`?E+?B!0W!PXNwf9dr z&)~CS%HZ0#*H4gWS&9cmi3XF5=IY32t-E2WXp}zdK?b{MYZ%qzB4rx~Fc-|fh{-9> zF*YfA;LqmsA99iE<7tM3jv4=omckRW3~ohz27)eMxO(BPPLZMamXNGy@>vP047rWF zb9#Cyf3*u&^eqqSpbLWnpXiZL6IQ|V9uB8Vz%iA>;&+qCyV3lAD|uD&oj-%kf2fB0 zXT)#V#MoC0T)h3+a6ZxEtRu+@0JyH8JGb#Gi*c94(%&k!xN;|=-hoK@XN%^Ii8>ac z_@kQ>N?(O=6kN@ENQopGM`JmIrtF-b$po7Cdp3F|Hu4~4RVGA0)-&n+6Q31vW}$Rt zG>@CdU=017_lpQtgsYag!|sAV6~26yu6;!=CLq=&rE+V_ z{05YwV-lKT37b$lApaPw4>Q#y6yhacj70(9Aa^d|xMy9xS7gYr2`3SGIX)Ww2_^q* zK|S?bwr8;p>7q&YPZ>0RRzp=T=|YDOr~oC_)1_R|>o0IJl(=WeBa?rgp0Tk*U;eJz zBv&`@p%1ZqKP`ee=&Bhw(>ug&Xsu$tjGG6xf95Q!<(>d{ws{$~<^Hsnz2&J8`jCs6 zud{_LA-&`7!!X5|Qk98gT_hp16$u{!06dH}o|BViW*R>k?E ziHbO3xq7?`b}3C&scqGuP^4#vAd;@!N%xr}N-Wa7)IWD{z1Y11Gx8revI!+U=_?G4 z@#L}#dr{qR^Uq3N!9t%U{PTsgb?$O8QDkljn7*#Jqz_u*InV9k3I+u#qR(tHj=MK9 zYFde<`<4inEZ5KiVRgt;p_tfeXRMbVR&iuhi!IcuAn~Ek=RVjL5kTiLzJyN_AIi2( zGbXzN1H{J0ER+6R{Bg)ZC0RMaiCm0WtSdUbO*~8`{Z+mf)!~cP!9%*tU`Te`_w(Et8QQ6>XHQ>tXCIFZyG0zPd;;kWXrqoWzzHd#CC_{asErBj|sf#4)xVv>Z zZ}|#;__I3D-R#I2(vU-Z{)S4?vfN1){T&&i#q6-N5`i#>`gK>ICpLXT#TawON=Y{* zZH2$XusTK)f;8=Ns95NorqYv>r4Nip9bnMr>4?>78laOq3U8<4bLlU+_CL0CWFeaJ z%tzqv^N1`xM(ifDSU{(D}|@#`<7k+^EZ_A``E`NH#pvMDlRb=afDk4$j#yh0VSp|TuF`5W<-kT5I5WJkDx6SZS!3ahf-1S?UY&oP1cN$5b^ zA^i9~ zNKIA|wopd^kc*(s1S#$-a(fj5_%)i6=iEY#6ynEamaT+G)emULYtqe42KX4D_)Xz? z`gaaN;o=;PNYH||4V7frgqWa0hiCEvvN5mT$6Q6B?so+sd*UX zi;i=VLM+`$EL`+h11(Z zT3O2;-HJVuz&S>?A~^u1YY)9pU}yUDiwC7iU6k2Z~`C zHsWW|k@7^L%T7|fjzOqHwJg0uq3jOLc4#JCzvutS3ccXgMFxW#l0FjVq>g}`JwyrB zlDg4-;qxq>8J{hZQRRju_#I_Ehvp?zjJ5#yu!U_~?`LM~1>RjL-qFNBto&5#lQ&M! z2F7YYTOZx#GRA-^dxowHZ8)sk;m@d{M!fylCRjLuq+e5$e?RYr`Aj2>ypKyo9EPEP z#qFO#rbf)x z;FFE67YSl57JL>f$ip8HkJC37EFW@SHWr?&t4XVYEAeU4iDq;Ouw+!W&au$8z(PyRU3DEf4?UT6i9Zj6r&cn* zJmT1mcwM2darD{dZWUse%Wv6#dLat3<~Iue#Q2{%6s<^@#rqzK@Shz<>he@;Ut*NP zX;s8W$8XJ8v*AI-5V2Td1IVe3yB}EHe#L{yqt2bOajM906A}0@`kTMLa8TVDA}}ve zu>GJ;X4{HN=>o@$0d6gEsmJUNEN+C!02M4GuU&9vH6IK+Jh4huBYy^5Ol7@(oEn8B zHu9WIVBV-eqb+mf7pw=~`usI9(D*4f^F1pC0i?bFNKO9i`o(&R?^FadZ{Lc@Jefnf zx4*gT6PRtMc>mKo9q)Xzv<+^7wc9ONc~g$T&GEJUFb;)<9 zb=4POB%v|EAEM@hLn?o`Y-=Q-!?(5vZaG7WETj8*JWJYNuz~?HyWt6M_J9r2Rw`CwRg^} zZi8V7<~_kw{j2eWZDAZC-^g7Iju&k`0=Rpeoyq z*#!aqs-U?RYZ^7shSCm(vlfeg=UB>G6+Pq09L8iIVaA3bE%_E~i}H)v>L=)gD3G`y zM_JTib{E9ip#%cz2~}48_c>tU>+;+*|7$%_8j0B3&`4O#*%R(jXdOO*W#2v${q4jU z@u0DIunbm{StKAEqOzL0&oTDjvN+RGBeNONf=pvp+AUx^)G;8I?+}IkA zC_+9L@qd*|M*mj9^6H<@enV5$s7(2Hh+`f`HrNaGX$QqaA1!3QsS>NMkEnRs(ufSw z@e~5Hu>NkQY?ZGk$}C#&iJB+!3;8A#VfL88gQMt z4&5nBMEMg86zGA~HV5U%MYDe!HHy1bK0nFfp9&fecNeJcrzIOLn+Vo+OBa+rrujV$pMWAyS8))Mqo8KP}&{}w7+_Y8mBQmH8-rs2Qsrvmr7U(-R=)+sVe;6k4b=B3tg=ZzQk^M)hQy>?}fI&~W>7X+` zW${#hV$81Ukj1^7;%Mqt^a_f$;A9r82h#_r8xQiGEbI9AAAEXjtFVKfNdOy^uY=W8 z+ZJ5vUm)@nRq?JdoB#i$;y-HoP0N5^JV7^Mvb)r(UnJa>HncVSC+@jD0jq9b?j0MdZ)x4BxFZX22 zKpdLPMEMJjpvfax+z-&nk85X*DE&W#Sirs-pfqA0huNjOQ;!23NkZ`!jHk3F&eX<$ zlUukY!92{ws1S<5b#9XTGtWoM+;M&Rt;71m=#P?_m~7!XOa&rg0e#k@naVu9Y{?Zu z*#^ZrRqkR`t#EBrp%*Tb@90X=^l7%IiK6l-GNiv2*uUOfx9(tN@z_I2Tlk1V zKzntF#=<|l21%7{GeTKRxx{ksZOQ;eq!cr3Ei)zAM2gUZFIm<)&4}ujlRoGE=HB>} z5D*5mfkIe@8=kaJrXVHXlwO_2Fg{~O7@*eHb9$cL|DD3n5qb{V-NsG=JvWlo_|0C?T}l zBnnpvnj^6`-hnv|$c!EM^@6Tv=*sG0cb9M@IyOLGEN(ka@`s{Ac^ty?yYF3p5BW4liew`7B9dXB>^&K9}i zBJ_rZVbK}sn)b~a1qN^H$a_tTqEstrbR&iNx=3ZZ3+f97TwLJ?)$CRx@S8mo%7-$C zMl$8v)x^`3Ar#V9YuWCf&~bcr{?w4VWBLS?u%(IOV8%GavF+MawU?k^q9l^nCHr{L z6JCj(rb#5HnHIT26x<-1BQ%SK6vdA-(zn@@k2@##nEJl^%+cm>H9W|2b;g`tDY3d> zPC0);{j5myh&4JXtWe*V%$Go{A_yy#Txx{#3r7>yP2Sd!x8!IC2sy$77e!}{#M$hG?z7DtlQ%mY(>MRNq1Pksb#lQ_{0`-nk4>Mg)TbMo%%_zgW7*nBXscgVe+(gfUw2yJ~J+OrkS&O^8JTk-Duh7 ze(W+>Mfd;5fM-M=ivUDs7DkT$bP)+eno;XHPzd8-Z2nB++~7fum=rS(9E>bbr_rUJ zh(*;1jieZ);j&45*^RNIZie{ATuipcWGk}0rNZlHfT`62 z2SVNKRv8*Z7$L8~HmB*+??K}ABb~uiGJ#iBSoQhfsgGCOg>F!@FSxRMr~ zH6cC11$dVmBh>U<-@}4I``+MS#7K;< zC9W6DUdQBrz6)Plz8FgL(>~oK%RgA#*c|Wr87D!kxsQ6Y_;ITh!#ddeRAsfIjAOxn z^53|W)*QV`=%>RJ960WriqwG~WB|uU{yt;Ji|zwBjiAmJ9mUz6sG?w1Hr(3160HQ9 zBvLU%*yv8xa#CUVv zML|#s?Z@cPj!7mTB3%;lR_xx0CLb1P`an=rZYih_tRdq_!uq^dzl)pJ5Bx51%8(6w zDX0Rslk{!=Ss3W1Wz=28upn4;Lgq|D9&^6=b_82yiRIu;po_l4$d+lyd9Ta~9$L+^ zM;Y>5_00|apkx?;Lf*1pGJc9{gFEOZ8;&{{P7AaxiaiuR48qW&3x<{C(|snFYYl^u zc%x%rOT33~u0bM!@2ZiK{V42()wEjWch7#p3)}@z_t@*}L15xoK1Uo+zjP?byS(_g zYo0rCO760%nSaL-Mft}Bf25FbG2Eu@>ic5WItn61#M@!sX*(~zG>^Yxywni)ka%)djMFqCQ0lxb17=f8^0A_ytP(J zyeRuB`zOG1ApOBwnlFKAEqb!N=jir6HF%ccLGi@meHZ>`u@&vArg2NU?n% zB_#j5WK@K049m6F->=oM%<7J9tu3WMjxY1GxCP+jR{`v2WgmPytzQgMzZvX$$I);U z(7yI=1ycwj?7aECFDueYhy_*r5PmN|3d7FbNf39?do?RF&n1b z22;oEwg`qfH=+~AbH!rJStco7awg#YISqAkHDUwGWhD!#?6DyHEUX?{75N>wsvdIS zN@Dm^K&!&cozz~&!QvIkD>LU$P4Uai6^2+YKR+db05Yz`^o1at-dyJf_jgWE-HG1SvM$vUS_Ox|PFC|f5}$6L ziZHr^u^7~!v^o?!W$$L(j3c7K$e0EYqWa>7ekvb$O21RIugzcwxQj;4?nAXDI3R+* zf~rQ!FdByB^Cx~ihtFillLtAvzT?8NxZ-xvqzmz1$0D?2>Su#VX~S8SFV&pC4@+0` zPO~^Sxg5l)I@g~~pc`r&hME}!D@c!oa_{-m==jj{B-P?t_wTF?y|mcLw~FyMukFX& z55%tIYJk%oOCmVUMUoDVRY0!j9q*?EEL%cd*vHr0;biD$?LLAOf{zW&hGs-eMG~{} z*I@t9>X4y@gp(3r)ive6Sq1-m1ajdCf03k|^PRliVfl)@n*Y|?c(e3&KN5Aca)OrWh+5pXVWD#|NX=oGJ6c-@w>7C~x&4t5H|6|)7|4Q^ zXjd)0k4EqBGrH$AqDpuBkUh>mdOb1bSFm~rBWhrD*El9%AtO>wGX z9J;c;Z`6R4m9~9r5-Xo^8PKgp%I*e@V(3D&iSDUwCV32k3rUoHx;#Pa?|L7YvMj$- z?)Q`%c%jyeCqq_Dkl#k4&o2m=W;OC?0l5??`%h5ISE4T{5frc;i)-E-eHQ=b?{h1Br<-BK{D-`V!WcEpA!oijf!ZKT}idHC6*B_gyNOTPt$1BLDj3l)Xt`S>XGDl?;Tndwsk!(&WFm(%oJ{raZ5C<2{C8&I*Z?xMsZqH9_SrcecR{@ z){E)qE4w!LJdC1!VG>SKd8ugTo6~6wJMVO-F;08I#OQFAl!T)!BZo7-%wez`g2K*N zPUyLnz2wVS)0d<#g$u1`A+8_dSS>eD1nEkr^8#>bmz%dOU$K^$`3Ckp+?Eit+DI?jyzbKl_>HJjnK$0kQ)D zZahJ4o?s$d>m?%qHNHNyl=9Vzw4(^|JJ4&Kn=To9Q3+a1eIp>wKE%fPQbVjwDv)a^8>&TW3hC zme%D383)T_`LDMbW{^c#V}0Qn)Rn0#U_~5MWja>zQ5>M2{}$i&jfk@C?;p&%ZG5tl z2QF*iA=^k$4d=R>=me_!M@eFfnQuZs!3~CJ{EKs(e1T+~;H?EyX!Q_|L;i3}U=)obj10AiUV|4`2B)aKur&|TFO24sRt1k;=(CPM6p12@nh^31_n>c1Kg zyeW!|Dsq4=1$r3C{5Z?eX?N{kfiEI{mk}!)hjpuykAu4kEs!jF>s&>C!;Mc$sYG9Z z6&B^JK;)Wb*j$x?URYqzumjXK4{}Ps7j$}6hY9G)CIy}pE?@MRI)4GSjhPLFh#HIh zFJ3im<|5&S6g`j0&j&{KMfwN|(Mb#(Va^vm=9+R+FyCnIKQaiS?kvHtaq1;=1;{q|| zg+zTb*`6p$q7;w;Vro=L`ErfSYJAYdI}I^cP53Jp>(hNNjo{q;mzFhEhz15uHx0oPd z{j3@U9I#}E>?^vFsI`|O*_j`3S}oxSG96k;xL7rejT;K3F7m95p z>cktYniKS?P=z-7m?GIF;vD%TV5Z7wKm7Bw;aDlKDzGfd8xK}&eaSx37rm00Yl{cG0iSK1Ugb)oGKLzvbR?Y7p+YWF?Vre6S3)Cv1i5U&og#Xm4aIbs=oF)1t!LvcPDoT^rZD9}xHGTa; zO}Ze_%z4PAKU$=Gn|(-})*zpmQJ2401MTS184Y8E3s*s}w;c>W z|EoA0Xzgb_8Z<9CN2V?S8=v*dPubSl)?PKykM}bY_qum4&z6x*{xFzv^WEx;*E+H zm#v#%(9!i|<$iiY*JfC4@H89v?Sscg7sI{z@TKN7ve|@^m8*Vi4Wk~DTDs@)^A+og z41o~>0y2U^xGEA=fuZ#rOv*g-k|L5N8n5!Xq+_4)EErV#VaiT1Uq$<@VL zJ6dqzF|#F_)@dMOl4H5waAJDyhS2E7nkHB$@Ymiz=M~Ec@breqfkUvZ-XU_*B4 zA}|v8zpM*17l~}1Vg$Ca%#ztXtiHYeK>*j*avI&wACL@pp&VEa zljXXCguumT;W_2Zfbx-sGT-fbtEx2IL~dSqSyWy_S+PIE)_(XLA>pk`!j~?Ycc0|twB&QN{|l3fbmZd6^# z-Jg7aWcS(0yp|iUj%!v^5vIyfSZ>GMGOH_xLqSmR?MZQzIdk<$ak&v8M@vLap|lRT zZy53jk?z0wP34s?BRBq;QZ&{jNg(OiEG+{*t%DUPT~D_j+)k&Rv$v;io`Zv`YM?P z@`+-8)%+AVaKn!k&FcWOn*CfAWdE(3;i*EiZUE`X`j_3eK>KfjReG07a0U?)*xrRoy=#Bo$z z3BPSR@YIPc%rBthe0J&d=p!Objq(pN^bK8+Yp}LOEY?qvI7o(%)&|Pp$8Aa#<7ip~ znPgil&6|c7)N?7`>bdc>34D0W`1}%D#I6>vu!a$wcFrwm`l&$reb~p}@0FT2tCoH+ zpMy`J*paqTcS8+lK;a1jvaCN}AAxt|4aS#X#jcLautmJEID;ixxTIf^G!`^A8Et$Z z%U%T22FM#=TH6_d42WCiWMx9Jcz}&^V_wyL)2bI)&JT}-Ed=v&aXuz$IA`d(uq3_WA)}D6 z42^!}g~|(xTtVuDil{&eRk!JU8xgtoSdTwUP@jqdV`?pmg?;PrhaACI7CQMHXF)}A z7Oe+4BI~i^zt)*maZgmP;%rfb`-25z8@Q9$95~-he!V6rt?|`-XqNfBGc)&@dP6^< zo$DsNKX!neaJRMZ0|g_^x72E1ToRRrQnd8JYPp6r35JDNNvvQt5s*yX?370y)nI`z zW%Uth&drmI$Hx7I!DZ)BO4W%CLE9|yf-Dx9B<*sHlsmb2j_2KbkKgYfWt5WGK))>4 zg@tFR)fo0_DM9+(DgBDDO*dp^5zH_Hp9uwJ#w`Go%LyFXkVAHj^Ukedf9zFMJ>YBU`aw?Q_p znv9xkfj$%b7Gv_@XqH1!^jHaA+Q&N=AxZczOmzN>tVg=jg0vu6H4_Y#HA0iwl z6De)~Y+dJs0$ySYZQQ+`b0myhw%KM4dJN9L%C0+7c?6bB)RHcq#m9`VIe}5ill9tg zUgihF(&VzUusZoV;r(Lyrk`|P@FQiga5XFxkwiKvb2ZCuO=0qsvl@=1LV783;j2Ln z`CCMMf$c$64jeIObH%5(bCbA&X(>Z?r{CKz#-C8D+SY}uDNPE^-x%$e+N)tl-bI`A zCTSTUU;pw=4GpOsAtJAJLSay4F)$EjO>Vsh8l4}<0y%>>rI`WF}_4vZvWDRXYX4#*cj z$7gq;qIEQM+BY3nI^pb?nocE7F^-CK-5xz;Zcd>6iqGSI)Zay;zhqF;J(*(V36{Vl zTT0>L<&hyjqcY6|E?rFL)oP%I!)vKxBqcPEsbW~pVv#EbUKC-<43pQe0uhv8U|*ve|ahMrM36;FibpHvArPn(r`fHC~W9IfpM-;-Uzg*s}b!E;KQ@c43q# zaJ^b1+9%m}=vpf?9h!4e-S+C^rk2}p`U?V^OE%2H543&Ugbq)s^B&AHw8^J_1lKi4 zqJ!2B{QlVO054EKD==9yoghnbi}e)=0}r600@<544zRIuKAkxp4uC|MkEd(;5#wyz z2!9dvm`5QhN>@{`n{rF3G{{HF6NK3+cSa5*45sb4x%Suynrgt>?E|LKb~ULA8{(MY zEoiP>wTcQ0HR#oeN$$Z;bE+iiMQyPKYuHMwF0>Dme;)zlJO1u#QA~P)Bd8V%^hZ~mcH}7-#yj?Q9e|g&c=6pjT={a3K~ro(uAbz$AZ7SHnDE*t3M%$Fk@6n!##03e3hlp z4I8g_#n7Lw;l=_FNin}Rs?@ysu17D+ClDn)R79KbH7Dxj4<#H4X9C6YtPp=s!Nu*( z(MdOo6{le7(I-xBigVlsN~h!SY2`JWLvO!(@#dZLYc-0N{nx1IU0dLxH}RZG;hO{R z#Jvw(9W+;-5d6=}rK0cTGDq6*Jclk24<_)xSPPO^JOnA@S1yqc(!iO4yRFC%W(L${ zG&9J1X{wC6;Rz)}AUOzjnXZ?acI0NQeTGMaPGw*FbdCb0F%i+M=L*KZ} zj9YY@k%ut!_-?t+T7CS1J@(6s?K1OXGn$Cs#4i%MPb9O~vPfts@>w00@+X^2{20vk zlZiM@OZpvp5~ocIiV;Yg6R14JNKh8cH*`@%k=An`$hql_JgV9DbbYJYCcN#~L@y_J zPN${E)r5EwX6VXj+>m1Q1%L;)^+D{*8wc*4ogmT6`NGpJv$%;g1d%s~(i(CocU?$T zNj*x*!YcGe^qd*Py|FIe(2sx2ICrq*e09xiZz4iVMVrPt@1xmJ8r^BV_g~4X- zWnqq@wOJ0f6(f;t&AmSwhlv>(Pf?VtvQ9Ua%XSM=AJgioCa}4k^-YM?NHy{njf}4# zw|@!u4~btnbrg&N`fIXPvk%resUi;aCMQ^ui8wN8t%hdMVMuzeMIvFrH+%ckcfDi@ zY~oZI4-0(S7^lRwgKCSIq+RwDF%DKTM>3+uUlVhDw!j__>tQPL<{xUm z_Ec^X2OX@7(+TT4zLxH{k=H_fonSrp2iFldMXg~kd@{k{fuCBY3J=U8W$1XZf3f`I zd*t^$6cES0lN%)FAY3c_5i#||x#Q7O(04PYRhGs7Qfyig1_7vfKcE?0Ue${E96!oa z94=RM(Q9;;c0&gXF7{;fs@#E7E+ORlf_2L)lm@|}3A`q=o85Zpi3EbOSk|jXgtvPy_$cUYDPxS?(PJFaI`!w= zdT?qRyDh2m@vz9Pr^`L68Zu{BQt*re0w5$X(0aE^EE zum3jwR@-Kjw&O6VIn;eS`8B>Q|31Q#kY8@aK+Zy#y4*U3xXb-?{IAwua-~&|AS304 zwbqJW|3AR(yRA&~e%bJvbFLTyh}Q@6rW1{}kkURs32t|M2KoPR^%ibX_D}Ti0!m1C zF0qUBQUcPsxF8+UAtjA;cQ-7J(jAi0-6`E2(nxoR?>^t(8`t&z33JWdpE)yg&bduT zZF@{v59NOqpbbiw8uEv|a(cp^%+1_Z!E|7}l0NzVU4X;HSY@k*C>?b9rwLDD4->j~WWlA;(!>`*~ zk-o+&c>imyxe--mhoun@-{?&#rIhI?WcZ7onNi1T}*$w*Jlsa_P-oW$KdZD@j@U`P*o^vUdFF;)}ty+MJzWc~? zZ0QySQME|C?hdV_lz3R)H&rhLvZ;^cCiXwZFGUZHNbGzY!K%{RE|{OYa;WxYA|^G+ zhBuY-XHIr(rJiqTVGUHT@AjR7WZdQ?8o5U}+-~Lb#yeG%`nDH3hR=B+kB5SwqgaqF z#g_r60dDWpMzQ@edm_vPTasGh*LQeJBXP_OGQN+A5|=#J08HxJ-IDr571TSaUx$wi zZ}x*k54d@*!Bxix<}{o2jHa}tOhvVn3Uwc217A%gRKn^!@skvF)e5ZFSl|4ikiDmQ zx%3@j^C!0sR0%PwxuYMwNnOSvjInPF4T!R$m1PvY{d?w=3IdWdB*scyCM!+?$PT(F za)v^LOhBT|ovKxe*r9GA;g;^-WR}*dQ$LO_o=9o736 z{JPn-L+Z)hLtjGHSBtC`P+CHhb82J*Z`XNST@MY!TL2SN@NN6z%-pWI1M5FMULyx} z{wqgcioLzde=}-yCX;f^(g0NA>B+=eXdVkh7W>-vLh*d^?_O;B-IuvlA~$w%pXB8l z#GaS>=iuP|AqezG*kLGM$d;6A%Y0C3Xw`$#_X|5vjR%s+fh&8-`DLGEQD#VmFbD8FZayomS~*5|sCJZ;(%N*DizcArqESy`p&y#oXiwu64)q z?;Rjn<>dZqZM}8&??~vSy}?<~GMa@gsGU~3^1J;CL632??SBM6*_?S6yE|IIz8WmZ1P3qzeCz$8lodRzILQOo9Utks9ABo3_*y+N zXn0$sXl+xRY6yL|PW85VhQ;u7dy>ERISR$@W3dH^!bR)FwbVp60@XxGx(Q-Lv;2z7 zWYDg}ZHEMNeqXf>Lr6#{>LEg(-^cSLxgXYRZcU-lLp?npEPB^UcX&#^gkUMWYcv?G2+yWoLWZd4(=a}M@_LTdkuC%1r82?9zS)!l z&9bA$`bJHo76`KwxOlvO(GOcErduFs(x>7Seh9AYJwa%!+*tNnI8py%F(3oF@^OU$xMKEVFWbdK8vF~} zy-N>W=Xu0gA5GfPmvnv)kr*oDOp;6a@VN}3+FodY1y8ZCZxT2}i-h|W_a4u)l&MFq z!Jm19*&6ddH>U+(EW+8VeFvRppnoJefvc?WN^qDUeKesl4RL!E62BK_LPc(aqn-M9 zXZeQb{BT*gFKvymd@B4+W5DydaqX_=KG%}*Ez#l#GdZoKdgDErLY*RWkVU%PVK>Q0 z*r%3`jMMshk}ThQ0ry80&Dc@K z7vDx)fpx{}R?)y4Pi$*y4c&Zi+Y6)<68rEmF7`8%D$BW=*Ln&gndQ1fD6?-3@|;gX1d1tXaLRw@aJvPnR6!cKyj0Y0)W!?-8EayhEmX81Z8PIZfdo?(}^ad zZK8&vgIf-(#A#q%4254kxsMHgh7L0nO0wh^cWa+S~^~8hrO{U zbrWIh|N5t}Qq;#-ChVblvmg12qBLLRVTsInX8MJQlP&D$wIuSdP=gPE|Kj$Dwn^-| zT1Bsh^Q9UR=!KGx5VU;j-laq6`nasB@~{`l{b4jsG}Mo$ZZ*HPXQUE}XUTx4cPPBwP zQ7$Jk%_v<7dRG@-_8T9oQ10;&mK+ax+Pxi!Lh|3YjG+|$AjQ)9y7`lgp}Bi{Vfg$4 zVcrc$TXD}ibK`B?!aW55yh6XUeN)HZWOv}XL4EJdm?YJjc5u{$5IR93*=vz@0}9|! znmnt`DKu*T1w-F-8B&b-X$~LjL+KSNasX%h$5+2hM@8$Y6DfVC!tv614#&0!&d;mC zhQb9Vjy7GJ=b+Ponw=HduE}aXz=uwmH%YPb;-)L;nfOMpx~5w&px35}ZsX34teiLLu1 zoD{kdaAam_lWF=O%$RY8#ZG}2V;?21xuQq9rsoT~KNkC|=6kVlsU2Yl8JOAEc61JoAcao6>MrL~|&1w8B{k1{@@QT_v zf(nuAD1XyTF-MLm{jPFG(fmK^PN5bqvP@kHXzWxeuF6y%!!bB6L*QeVA}$fL+CGyCs#wv4 zOHNxDunzQk>!vHrx#`|=hy)8A{tn~wD;ur>RjG9*hPCm{4v;PbQS>C!bYUYC*Ws%3 zsKWU!VpE|0NCY}b_}f0F>Zt5HvF-@T5cD9|qUGMDmx7Ks0!v+~`lmB0pPRa4CcbO3 z9+xy63w~qYe9UJl>Xc{DL$#BzGq<8<>3EWW1<^WhR#r*m?Z#ZQ!C6P(!50v3=aWj6 zeVUg?q-1SZ@LZ5=%F!36O{a4HL5!mBQd&a^wv7mdHj%U~e~xfGPr`we`fonJ8?k4v zMw!U4lyv74ig_6SLdYG{rAXT~f(pjHF`Ai09qZ~->f9z%oiPx1)ceF1Kq3{95qGHe zOxESZ?asFL9{4+qj=U&6b`RM7`4yJvQi~5r*Hedt0AEz^_rUkB!Cs<%@3U3>Wq>fK zgDER0>2XQ7GUQ#OUJx%W{D%(H09;tQeI<;ML)dyZZNG!dNs=#_a!%`8(VOzB7x9O=mzl2dd#- zx@^99XvH4ZTtFg(RD7Jr=eE~Z>yU5IQTDC3?%%7ildVq*A6kaVC zUTm~@Sg2_b5Z3V*80l;&5kal^rS^5K|5|(7GnS4Vt7wP~aYRDkP{44C+?NO@e-Ld2 z=^`wyB6S;|1#mXQSqpdg0>aC1f)a!9vx=|xxE}D~GQxMh7Y5QT@W+_&?I8^))6LKK zhM4AdE~cYCM6gJAU-7t!otdJ?&*X%h!|666swdJNe}Xg<2$xhZ*v`n_|A^z3FD!C{ z44ATNMlpqxoU-J-ZahlENLOGxk9y<(L5CAtlU@v$l*R-9FGolPzAX~MIlxBHc>qDZ zQOw7;V~?!-4l)Zt7E&4e>9G@lwz>7yhjRWH%rE$e9+oy&%{{)cEz-h%X$H*LaLLSQ z*+oCsuAjcJBzXPJIuVDDR#z&Mbkp1&3zeyMS3vUAn;AC>`Wd?gs^;b&c7O{ue17#v zl#sin(c(XQ0lR-Jc3T|UY z+z74zu@9?`^)8KZS0Or&W3*C}vXweHt3+Bjm8?iHD2fZqE{W8dfeN@Wk*?y<>U+{6(z0MfTJ z?^4(ofS4GvuxKx`*ZF=Z?z#l)J=;TlwIcXYQ3o;ueNa>DgXUj;&z<)KdVakwLDF~d zig4wl=g*k1hO)Nn-vMELA^yYxpsmZ5PLl}l2$`7NCl90uvSY03*wP&N zn7(cR)x)K%*0&i(+dI07!{r%kla#xNl(&wMQv8&txQ=S9CA$oZcPb?YXyc~fbRZ>9 zk3t}OZJu?7jCSroSFD3~egx}Mk* zc-uCIHdjL3`AH&dUL!)Kp9V@BwZ1yKTMyxYzZc(K5W|T^%CPvr4ZYmCqQ6XT){YD| z>qxOFKs0lU>#F0(Y?gtW;N9-q&1JrO3D$&%G;|f;XVVY$1-+W6Bdc-DsjROSzJSi} zJWa)QRW4NT8SUQSQ%$wExcUS)qE7j=HI*%G$JZ8`&k`V^jQzb7=5USX-)K$wR791j zY)&2zacfv@9^pFn0OtI3_|J^516O@wDGzS#eS42 zC$`bWK!XUPuPnt|+ypm~Z5PkeEiqqpKKv9*T?w9t@$~)5J_e`n@0O6tE=hOXx9Rwl zPksyi{`bjMfABDT0odw%SRz&X$xni$O_0W*p!}->cU?iM%q%iI8A)$x<-rsah*La* zxq>a0i6MJ5@Lj1l^~KyTeD@%?S|9o5XQKgHab*kDZrvl9DZ2LBhJ5dU{E+;QwlfVn zYhc#z{;RGlv}ro-B9N?enQzW{h3My+N(jOkFqv=Kc1W;|^1$6sTVZ135%z1Y`+^dl`C;6$#G{0nmUkcl(ZKFRmQ52kWnm!(2S@iqcum#GQi0XwKWmYD7~ z(FfeJw1=BqWc0diUIXy2gY-bGap?jt*Jav;bHFR_`K+{nfa9p?($H$(5F6Prvy{{NWRm=bG@>~c5AmIp73wpE zps&wkcLxS57>i_6Uvk8GY5q9l?|Yy}Rqt-wP{!DcOn%~A=;u5z=bQ>FAz5_K85azc ztLi}YyDIhzEl)lqq05+Y!7Ph)cbLQ9;0|uYXigrr<9&zw*P&pP<0Ie{iZuJiTsUQ1 z$8C1@@9nfY>5qv~X$htV$M1MLw8N^wU*d)1{bHh=u_viYzfHWYbr}R^bX<7e|Jgsa zPu~UQd7l2dvA_M}B3VL~nV+{sXT~tFJ|H!~PT)ES&3aB^E@c@B!yscSBR(|}x<;ZU zK1i&vqj{_rpE@VX!!}Uta@64j{5PK^(b%X&=+o(Fj4F=}f)uZEej5BhH$rv#-_~+i zhDS?M59>nh*nahNy1c3zd?zL&H3X}E3CBnwRlvtkt2@r>mxov4aZRis2yk^KhqmnA z9BJk%+)GE}sbL91UCk;YdgY<% zLNndwXRU;%1k+>U1>}lZZOG%9?FQk5#yhg6m^mYZW_(12x9g9+g2GAB`&M(_JZ`*g z-&S^~sy@Ax2BAfAHUr)5vWEHUj_Z@Hos_8YPAJ^(7k6*=3f;DhPe7&m;N_(*9CH^z ztjt}6%_5G;if=xCr9Z{b5{0NFVylyna6iR3k$VyM`xi7b_od3XO^cZFGzZbtuY{oK z!d!E&ovhAZDjZsRrC6JSD%2o#P{Nex86Lj1>WRz1R%L+-+SnV^xR~4*9hrIQ7IDkx z6p%=EluA`DQ<~uk40rHA@V~_Qx1P4I5i^8X{H44y z)m01`kR%_Tx@Ubj7%5zOhERX)T>}HDP!r_3X9PACiRy#;TB=%&^f*L zv9c1oMx!AOw@A;em$~Wqij;rQn~G|x_U95+QSQ%g8D<9-B*k6`Zz;LY=gZ{s*K}pC z`RLxr(|DbGog-Bk+$fevVEyu=?t3_UBC~k5%|+tAn8UFkig?x4aSP6A%b9D_ zX{hzE$E&GFx1Q{mYZUi~-A4MOR}|CzF%jz0OGK}K8?(>O3Y~i+7O>sGuG$bhcUoW^LS`NT9R2NF*aD2H(PFJ zRH}jMgd5Iv=%SBi_-NjVi=a1!I;w^9Nf_Surfvk%{Jn=%C_-5>GzXX7*4L(DY04072LCYgK0mwKi&(> z7Gks%`SWIKJWxBczgt_~EI+0^lM0ikXE(|OyfE~EJKbK<;(Rd(xBHl*Gq880Ej*G$ z^+J-lP2m7ct}zhuNqA6B=|`>gwqgfq>gn8TZ{y@+3wZEfoO$V9_nn>Gtold2zeZ+s zmJ!aMVDhYAyZxappGpru*=Pln6;CC20D$j(e&Z&Lqf`1p zjT}3b)P+-Wm%H^rhts*`8^3m{A`lQ3-=DfIfnr0rs*C9@&q2RQBO!jHbi)K4wPod2 z=%OFQ3~c``X2!alOO0RgBM^;#&H5|9wBQ>wiMl@)B<4OAchcIyq0$3`)G_wu@l8Vn z#(n<>vtp>&98vzao+dqCjx)#|N+MO>(qkv6(&c%J&k27A82?X!P5%R|&oIqdn>;uA zjVDNkR7?-{$0zjeVA?1D00s{ecEwxuW>*#)+mY*IFfGr69rzpU!W!2mS@wj^+7(8o zVCn~3VPc+yzSo!EC2O~7!u1Y)I#b#)YB(QI26F4R(-+hC$}Xa@*HZszyp6&nQvR-` zyhAzPE?xV!C8^=!CGv;Ag-4`BXt~F${s-gpqdT$tM6a#o8AcVEeIc=f9x}<$kJMkB zX3<*N9jjs@u_!_Od%>e3ADL(*;f?}*KDMIlk?E0D&taY7c_M;&sZ^6~5D|wFoo~lx zstvOe(;d$1M42+?Wr7-L(q#r{r?8fmnE|P}2;EP%BaJok)%T0^qqu{d!r4(fZ~U_z zSK?s}GX9^_Rs*+mX1Wh4tAfOy6aDBP1b5yK;riY)wB4$8Z$=GlJJ84I7^ zhk|t@!m)9yoam!1<-e+$tH19{k}o%C!GquzJ=fzS<18<|6%uH{xZY7dEJ(C zK&x!tVwpTQZ%=P<8%SZ_w;b5A*c}E)R8aBgt__8;FD=Jw3>+U6t zKdG%~pA)nDa<#J19C9UG**1$9hO8m$=Mq3h>Q+Sx!_cnG`lGh=0_N9ES7r)pnyoH% zP_k7Ym79d2!$wbPwu}tO^MJ9--GC@AYO8bOX{I!kb<$SbrKe>=7PV|xB zd8Zw>M9(xXaipnyd;#+S>rM4FQEp4>jMZ|_zYirzg_4=7X9+ScEgD?9bG(ie+CvwH0_By8nX*D{g?>nyrGSh4LileTxLzQsy zj1KDml*gvi;Fk)jD1_#ri9;){oz1D@OThAVjowz!N&pqzHXd{2YQ2#^Sobt7TCDHa zSrV^IM<{iL1)^g-u9|O-w~B>yu$-@Z(RB6Vc7}TxOC0FUN-|qI(Q6Ym(;4ge?^-3% z@pii~CimRz97W&2wd&=UJ|n+Hqh}cr^xF@6OE;DGYXOO`S4>e6>N9qu*<*g%CcCr~ zxZ(yNXX#D*lM@F@J*5!EsrDC3b81l7P9r#tCbOyP?ju+EuGF#i8&?E5PW0Y~bZjJ_ zCf&l}4cH`%ch+!Br9fq<(~O}b^xhJnU=x`s z$Y4|feUk$wMjv5{QCSLJ&MhY?a;M6-Bss~%x|8A6lFlWln;J_?J9hYw`1jjiDs6ZW zdOzM$F1f!_FP1`lb_1qpqKo<|qoCc)kiw~=gfSl_SIoh9`%=2dW$^#6HYFsn0Ox&D zaXpApa>wpTKrmsStx8Pwsu{*FA3bOSAO@r_0G)ZlIhDC))<=eKM`NgjZ*UJ(G5LqxqD}Sa#&u& zmVe+zB=#^ou~kTFh_T)1mqG0F(iJ=VOD0ry=bRnRu{ySZ^7z2#jMGsXd7hMQfzjEH5?E=6-Z=0B+%Ba@PvY zs;URL#+=R8yDG$K6*-@S^?hHsMLsPYJL{W|0XcxI%`mosl5xGNw~i{DA=O&gp_I~B^@G43TRFm`d)%6jx)tBK^zxP4TGbi`cQB~kW{3?T0EP}vGLFoO1Iy^D=|DJo`JytkQED48G zuonCquOT&EaY3&T->Tqry>D~Cf+d=1xsVlRtT$LC0~%+ruh??g zZ@yg*;6{ryJ+LFpGx@gBpFB#m<;qIGoZJdqAoKUT+}$9jUE^8eHt^o!b-xH%Q12YM z>zNjvix9o_z>!tZ2vE}(@TqDL_Pg)xt$G&~&BQ(HaHx`Ls?JJ6@AL%Pr?DO(A$AeM zqzrnENov9Yog|r>!!GyO%Jgqi#0fqu<-wJ1hP>(b&zgr- zhg!3?`{(lW?W-ylM`!xq@O_GnMf!gSgJH#AEq%Sm;B1BEuK0c_qDxYf=tEvR-+4_0 zGsFtL#8PBn{|+2m65d6u2^c+i=e_#!(*vc#A6(f@hPbF0?Ezw-( z?8A>wP9EbYl@(!kn_j&juv88__Lhw!IPjYcxMEiMCIOaBd<#S1KN%8#AxC=MQc9H1 zF_a%rV|~-TBciK1FDUMV#9lyVilKjxfPGOD?QhOHt~!Jmqx=f8=Dj=glv)(EcZNu$ z?@@;Ql8V44RjzPiE~Pk`QvQTk$6rEuzhgc9^qU3F#1&x-&-Hzf?PR3Qt4=4@jJC%_ z9c&2&?J(H^9BsSWB)&k==F<<&x333-?ez!K;t(J_noM5)vl`#%Cad2CO}8&}Lg!0u zy){3GpyBcW_I`&=g`-%Tm7~7V`c!@4@?gk7fKJv#vVfl8Uh*j7q}dA8^ZQ+Y{^WQ4nq88(!|k86``yW1X%D zvlYn<(;Bwl)xpK3W;DV?4M#>3}xnL2yQirKX&bTt!ML%Qzjh$ zFLaF;8PN9dNO?Zj`K(K&+Ca4AsO|-gDl_%H15#>?&)HETSlx z5`_qL65Ums3ZKMjc~`wH9jnY zsrU@4Aktk3unLi~+jujKa7&2Khy_CK%PrvIt2ObgeZwST?pts$xv;g|{sf#Ia9+** zqH=efhc#z`PYnOz^(OiI!fwf3td}UZn>XK8W+ipCu$afbkNde0X~=!GV!)}&-lLA1 zsoX1>{5Om2b)XE*K;6jjWZaLm+|q&b(8#jqeDt@mnwf@YVG^5CJ1Ed7g)L_`ElUz@ zzAvkyDEp*qh2A%F*}+cx;IE_7?Ac?XyA9GVIFI$~np`nf5Rh2Sr|guEY|Vfk^Q)L~bgk3hB&Yc$xifhRI{$(dmlbg7nRfB$)}eS+rK& z>$;^od)5Zl@-cHX*vg;W6wEhoy+mIx7^KPpeA=E)CpJUO&v_&H_2qVk=v(%TOj}%{ zd}lc?w&Tb^q9BdYM$@SbPSm!_X`lo0e4aW<@xF9@tcAXi*2~4KB$~r&BJ1wJWC3^H z>#rrYQ2gT!>6Q|Y+f`|Tf$4&Zl}Z<0j9}#(*>Y2CE|2T&1yCYKHXyItO4++& zCBsUwbE;|~%@$N*{)F95#}}d{(EAymW`ZfzIx_&N+Hlk{_7Me&-+8tVekD2mn(uc2 z{{<%EvE>lHilkI5QbB%EG4aYr;c@J`DY)K2!%n2qp!mng?drk=U8dBh#kK-BNUWhF zS?8JY{!*FN9>j-CYH>G^O*22C+~IuAG9N_oAMaYaV%++*#F{B;Rdg(fmcN((=I6G3 zzE5Q?ee;jxf0o7ReEk(elT~2XQ@FQ0Qo0n=qT&AW>H8J zjp*i4iCFM2{>%89(8tw%O01l}SNC=>#8+{1F#TF#vwBP$9 zi{NN(d;)krlYwi`yxkg?+oqDfmMsvN>3_oiiCRYU2nRTez+SxQQ0<4-&N>4t*N;yir5K6$~Q zWlTBXPaVYP{7SGi5UV}_uL|ktQf8jKX@C7V!kLv;RE_LY8uk;f-ml$g+prjvx2wP) zJs22m)O(9Vu7pqOC`@(7e z7hAJe>}A?VJqy~-&=-w}k9gf@-&|FxmRtKM&bMvSm{*T8(Q!$;H$2jjs(c8CqMVq&|PbJtJ+@_nSUX>8A zzRf8T;+qWoOUAv|GPx3n{&QrBFC2wludF*SQvQEU28yOKIvZycSo#@<2JeyF?J@i1 z(fVljhJAXs#Kk8V^T&_=Ph<1bdBC;dV+2lRZe9svYDq9MXHa}L3CK$8gXvIHTdVSg zW_~U{$q1ZdO@+WzXuFE#k>+s?V~=h;jZ8*ygJ!jFFF#E|2mn=j zd*M-n-ly04aY|vc67M3)=u{q{uML&lp5ZbJI-BPoyR$WS4v5o~g@R5A;E)fU{&y$e za4Lm--q`*7qrEHE+9G|?Z*JJz$cZ$Uxauk0XyhP*oB;X(L9FpZQ!8`|qg`_Cz{ok_ z>6hq7^NAO6;N)1ZvJU1o_dR>E`zw+N)qeMhWU483<%9)Pd4v_J? z+W|1=Cwfzug*K^`;|2bH$LTy$CV>g(LEA4>RD*ch#%7%c< zc}{7^w=Ar~iUc9WP;kG5GahP>wsEJ2CLQwS@`~{EdcrHcu;>9IJSab~1mw^ZrGCky zPdg0CB*jg|i>i8VX4VzTj1~%<){|f?PZCvcy{zWc9tzf~LT%jB()ZmgD;tkDZzr;X zR?Y;a(v`pD+ehAij^2;w_i#vJOY3>;9ZFu7!e;%Ajxux*nDsuDMq;Q(>(jMK=rNVR zR35ZGOlaWR-RCEk5^QBFKXHz*$ld5Wn}yWIfMcdH%EhA@RYcB8@Bvx==T$L}qu=(& z6{(3%f#hZXy8E=NbrE^i^;pmKDE#g-5P*NmP~FX1z+Y}0RvY^!Za3mXyH&Nj z*PCI5FcF~-weyr87_gs}?)Ht*`g*fb?VkP!G@bl}wgiRW>HUY7?x_M zy-zHe=)_~Q$|8S!#SS|c6s@xnAfk+sf5_hZ75DT+?v zub*dnRqjJl_orbv9Qu3%T@~h);?psj%to9J+^&q@cCUW$Z%o!rJ}Wzj2g^ZpW~&h? zZbIMAA6WW-9;~`ok9;1S=d&g-@N z#e25(RL+CI0}M+wBF5sF!BQF0t~h6|?rju~E)g8nJnbw!9^oAufW;8GAWGwg3r=No zW=P$F?p} zxbNShrGzHD9oe3%pPiyEjm^fGN0s`HwB(t|&oapT5YlCz~P);)Ls*i z>3T7Kq^?)N=*KIQRwyeY4MRb$!kb?dIHC5XbNd$00^aCW8Ks6hOH6!2Vf&tvq9fO( zgB(4SRfdjV$+ls_)D;(Ix7PIf(au~gj#1?n1}fC6T|44R8b!q8O>a_Vf`#mkG9btO zJCA8XPV)t#)BvX1Ad7-gVCOwIJJP;m&CGlM-XZQjS=+CI-`v!Dv;N&@56Ha#DT37% zHDMS?n(enXwofp1L}%(fo%%Y5MpW`gPQ>EaS<7p+Q?%&1u-WW#ZSjU%>*%4yIgxqt zk)%v5ej$MHd%3|LZPW;om6V8-HhhOx8h(E<|7to5XL3VHhtZ1E?qto&Ag;Z^+whdi z`qlrjN3b2J(6>@R4v1elhIjw3^W1UfD@aBkFusA&Pc;rpJ)`xBdGG4lAJ)qu7JB{M zoqfJ1dk)>yTa;m@SsHGS{I6J|0@Tqsp#CCFa$7ih#b}`%XP^uTF$J2@pR0GhUd~Dt;29Es>5wCUnwM5 zrqtIGUAsW?Tjk#_7S-34UeiY3EqJhazKEX$`t^}aM66Dm&_tMsc!pmK9UN_LB^DBKPP8VFcMsW$P*Z^SW5GSDnFY z{QA_a&Q}PFgm(g82RWFvS#=^+jW>Fc)gU@W(F0o+5oEQ?YCV^T@1kfTv4 zQl|P7Qbee7aEpTfuix;QC1On61bcsw&-VP2cgMr>-t-TMwON5F+}VK+*X2^aj3?r$pCGcxx-S1hBLxOz zSRZO-`wPqcnpoTDg94rQ87j5!IokQD(Af+w1xQ{;Cd8>h>+4I+1_R(ln6uN1)Ua4) zAA!OBiRxY?_(oewzA ze>FP}*Ak;%wV`?iv!?DEOo1c&=Ak3cp>(nF%|G`nuvfVNE_=`X=tp%ILDt=IUwE5C z2X!XOrDLlF%C=2VSRO&hucdlNh-}Ftzm#tOP`PR@0)zPM*KRbs%}<;9cgaCIXTFTL z=RsmQg6dL3+>F%E2S^#mE@zMdXO9$Kt!+h+xxzQ@NB?>VZwT_Qs6zG5OH%!O5v}Kd z-jfm47BSM}Wclkj8Wdq)C0pY{>L#}5AXKc0eF7CiI)p`X8LJp-<3`p64@<}x0VKOi z#y8#SO3J*gUeX5WrPx#`!(;#7Zpj(1Ufu`SV`nFSF$eANSyJ9re{b{T{YF4BO>208 zm-8!_b;itGRN;AI_+r^0fn$<%X4P>#koirn{q?TP%j(ka>XLHefA3@)JmE>xNFdz^ zXUH9wd$g%srpSR}HdyEJ72@dY)chICd#_S=8*vGJEWx9GS(j5Eq-qZB`*`Ep=TxKa z2K|+w2*%dBxdvcMN1vw9{&0i+$QZZn<$jSUsJoA&5Ou{ceS0&}QYJh5Go?mQ4|=>D zqAsvp7-kHhJN{A6**k3$tY#~Efc5^{#kffvMku z6)5s~?89x-ByuN9pAo1{Mkf<;C!aS3&BGzZ_=ca?DDHn{zVAikG#xX(9>&>3c3P zJF(m$W5SF-fn+_gU-_e*GES|Wm?LuDu9q9awzsTYv1{zTPX^V)qVQBKt(vW10r&gl zt##u#j7FjeOgcLD!21#hJ~d}be1U61mpdK~t8p|3gzfO=AuJoN8_ZeT{}=3gIXkrs z$;pFBaC6!3+j(>ojj}Z{s#%JhCadU**6StO-ZRTuIpFx8K7R@x%}xo8Ot%pN-~Dl1 zD+KBKe8(%JyuZ$D>?;1+DWI~ik{fon{ukhT-b1OyC|M?xbl42N}&6vBN$HeA+@EUjp;hL3whtU$-feUecf^yDwJ0U;4lRy2uD=gso zS{CJ_6b))JfN8oPq30)bghMLx%v=HYYa5*+v}?ZYI9p*H?bu@O_{SOf+K6+p+JpfK z`Pf$AuhJ`AQiEnHw&+@!c)fpVP>NGrI8!6=@Fyk9~&!b;epNZ5SfJm-Jm~`g86sd{D3L%?IJsW!2nl!7xOfzDi z=Czmp7I6ZJU&_YiU^rL!%9YH!w7NjHH;LM6o zGs-CaIj>mI2{QQ4*+yTzFrnt7mb89LitFlIQ%*rOIK&?e0MKgzmlDVYxX%w)z8W+8 zq#%OqKm>VVOP)7KynQ-|Iq6-gj}{lYL-)lamHpp6l1{Vy%?B}Vwz-U1RARDP+1O3(yF;AH6(#e`8%hOaPBHFmg)A#!Tl1`Pj7oDfSb zyJ+|;=q|t!cl4@Zx?2vm!l+Aca#UAnayLBh zM>xn7a)VovugN}xro2;ryok>?TEYPQiOOAjRb=ZiIEhPdZrz2f)ds|D8u4)7*gh8{ ztWf`PaGp;iD`V^UWSgB9;4F~;s({LrZ4K{}@}NdGEKM_KU$wfo7zjM1NI{~RocxJ|(`=vr9$VjQE&06P@0$5i2jGkj z0K|zrvmmymXxCBZ&EydT zYMT1-&7`k7IZUMVcf&Qc*3qx9Aq|?=7oT7ofwk(P!)MdreF$GDwA$pL;V-C(-C9Z)v^P}JG@T1oO(|n-YACSJVCcT9| zqn1)7MBU30yJm|b4%$mpdi2P*dOfhD`n$=EW-F3C^K(QVV7>_DhV>`z9 ztybub@$k9UJ+Abd?(dx>TO8#gSBvFvt+h-{k2P4q5_l+0ir-!=ZTJP?8%`RW2SJMV8U@~kvUPzZboAF-t!!B1K`)1 z3EvK}s&+n0eIO~qe?XqtpKhPZS(eFynw3_vHBj=QRjDxetg(x`4A_#)3oX>yk>=di zsVk*nn8o=jC!2}GE7e=d)fjXEk>Rpo$KPz_!{F@L?+jgb2RGkkQ-Jj?`}jX3jCPHG zaXQG+pV$_4CbJ6*5CN^K(o-1tqYhT%4k%6xVxFrH)>xndRi`P&4dVOB{tS%hnUmhrYJ9>``C|I77_^gDGWJP>X2zMhbe0_Q>_b&;$!9dwxgdY^n6e1-0 z#C*gr&~npQ-=C|XS0bfC>ax7b*!q$#=pbvKDT1+zOrub;&UJHPB^FFr8fGv#Vn&At zQoH-?qtLnw1WMf$#RVUyt`MSh^=>@nCW*((HbkfTT~oI`2i>)qYhTH|Jx}{e zH!T+21Ux{)pQv$a{h^F(|hC8VX1?(PtfmJaEZ^ZNh3b1u%s+|KXad#$~CJ5 zhzSh#`YXKG`8sy3NX(2-{LWtbMsT|SJ>??J(p$OO=Qmt5#&ibcVNG6fcG1yaLj*oX zY%GW#t~l@2SDPi72G`^4!=9n?Gnz9TEIPA?#Wk>&ji8x7;`tO?Q2K5e*d(C z$=b4gSuYRBcC2qc1bTB90@xX%|!?yILV= zFjLC1{mG-pgrYrlSKy}!rfi$fKs`=%Phyygzb&GU;rp}W%DnFP48mEP3S@+2RG-MH zfilch5vWn=1t-;#@^(=UkZ9BvZ*y*-ivMpm_XMR{@^v%h$Vv+>tLQ>JkJ(vCVfECw z9C!K2p>Of8ALKY(9#h_nPS3f#qxpo6f5;+Eepp00L4f1|!=dUL$b;oS%FYhG+ipnT zEZzTwBMjRhMKFCk#QBQx&ocdKV)G$J!s4F8CVOF$cjV2p`YM!&s70um4VTB8%v)|Q zdT?7L^hZ9G3-Vp_tygSBpfB3HV|k(k`xVAnwx~qk55n6y)wHHRe4gis-cuiH_wD+d z4yv9+VMbw+ftNq_x)XU(;zptVu~bd=E(f9qcWEGE+OQ<&U4cNvRJZ2xI}ych4h>*V zKpt6AwZ!XYRwrRerL_C+^%vz^-!IB2Z@RZUmnZiMxxIWpT86K`;rl#$eH|pfq`Hz3 zQw8fQQdnq>W|(iq?&TW0xb4gc(~UmWfy62HB(zt<1;4_R=db`5bzX=XSa zRa#;AyruKe5$jg(S0^N3N>zqIK`(`N$T5H4PI9+|HJ6LGHkj6|exSJE3JX}a(jDQDE54*0D$l)B4P$I4*Rh?+@Qr_7rI zY{_TT(KqGc*|WBV_~-eF4==G9;B|~lH#PRb?j(p!r*yT^_HPG}FbS4TdrQOefBw}t zWh`X4icp5^a%CKBmRtPvLivr~fCEH;QaM8Ih|jk~POgawZ4hdSc-18HBa%}X5&*IjO|6klp5%GXOV z-F_rXvQyJCwdEH0ZZ*2!v+AEi6O0j*ti@ja%wG!NiRL zq&~YG$-h2TVkD^i(G_yJlqB_&tKyx@ZJOOz8DRyU;0)uZ%H4a={oDi7r?jFdV?sd$ zbf6}s;dMd9QQvKRtKK+V4z2AX_tvs2g;!8^#IPyX42^~Z&VZN# zrs`|wlNhE>e(znI8Bs-8AjQ~pozkrO*JEtKBa~X~B|%{h4T}1b%lBFKG8#g5jOU3} zlW%M9MjhwbAwt;2ahN|vF78w`+Imakn9RsPu!(1^;(k;oS%j+P6BGtusOYzGfF6GT z462*1)GKk-dVd`0@6{^*wu~w}!uH2)G5;Hd_+W$0uWD-T-VQUW!T?75_;(G{`KEkqAyRc{n4$s)=f{qAZUe35!b641sq~}O{;YeB{SvLN zn(O9eS%*cvYD=Uz+Xw6qMCwCIMPAhPOfw3xtfEQ8w=(>MYYtK-k*S&1 zm@ombeewGxa<2N@+Aw#&3?toadQxfE(K7z<18mwxlr)8;<=@N$DS(#niwv2m+J5R^ zT7FPspGlGQm!H(XFSt?Q_owp0T&-%pj%Tz82zr@>EEaYxpK?FH>4KLrz!z>!HK8`=KtK?qsB7a!&vBW(mF3S3yN;RjL zr!9QPpnx$@$(_VpOf~(qjT=*BQ=k`?&i!)jPRP+Up6Y_1G9;kOt>~>6vg` z1)*i=U%`1&kV~~}B!2z5h;Pl19U;H7wle2@c4E_vrYXdAClUCI)9i76_qd+hrDOtR zsF8#H%bNLyD1r3px4Ci&xLNvFeGhmyho>73EUM`>&(0XCuARDt_=q*$X?aI0mlc*U0n|Z=6?1iEixq8m_J86Hm7}iN zH=o&S43ReFmt?xbUN3H#A<;vFn_Rp;a@#+gJw)HPow-m-Hf>9x47<130Uu!t}-*-S{d?3m=gVCo-`3CPb&lYcpnAu z{L(yMf9=x7?&@we&{HaPI!@Yxx;t^O=k*FTX%3at9D%xnQss5341Yt*4XWLhzqh?; z8XOYIm+{wkbdSP`62phbuAqyz^s7Tyb;@CHc0glh0g+3XeE9&ZAL9p$w=wnu4_hGn zjAb0NArzhJQ6)Njylodvp^7(S(i`5pm81nuym48as+m2lykEE0GLJuRbKl!oZ=uTm z!07vt$JnqISx0qr;D$Vb5az$~E8Py0IVt}nkta@A|K8V9-<|9GP=^@+W6e{8FY+ZHEnx6rFK$LUAkt$UKUdt4s=Kt=vFKKT z8=XXsH+h?L2Wv8f;%NglqRzDaJWniZ4=yrOx>PeKuZxTz{s&J!2>`g$YHfbK;Tmb5 zM1>idbKdW#Ai7u>YqDpQ9i=KI+wm-G3s%OV@kjd5cv4_nmVA-=3=4C)(+yw~y4e9Z zzMt)-q1iZ3#M(Y*t#>{8=*I!p>)bLek7?9NfAIP`%vJkY-NT!4-mPoN97za72)DRzKy7q83zA|5J#Li)?+ zMbzsyl1i;j%AGL-fI$GDRC;))I%>4p%pt6xinU_J62-m=fQ4+!YLkiz_t52j)cr6b z!AK_%vEcEmK1Kw8?SlH{sl6Wdg*KZF&7D04%#SP*XVsO2PFvbehKDU;^3x#LmTffj zeaDB`gU15&&l=Wr?I<1*&WdV2G|;1H{V#v%AeRvl{Lxu(5PL9q5YRido&c3vq@1;Y zj)yQ>!-7J=IG6GzICCL7t&<7O_sfDpMsh=8OMSH!ILtR=zr-kP)JxtQjFlEIQnWT) z+%2#xQ-ZEU%L+C?c3V}7=RKaUb32o$#cjMw%k=$?_YcTaJq=SYYcdVb%#|5h_+wDJ zNc%w4#UIOgBg9^fyxI&}bW1s;VYl+BiUbKv&G5pi5Z3a-Dms^-q^kkhrTSC=c9|qw zxvS&XkDupJtZas!6Cf-3Mc%hcXt7YDp*S24z6+z~hU+8r!gIq%SWJ~Fr{qut-kyLL z00ur>gORfjOa5mrSVN|>qXogZ;*1h*3L2fCcD+7oX`Ul~DA0ljZ9)99+(TL{q+OvU zlYz0M^;Ghcc9lj{rXs8V6LieQZmZqDSrM9DE^jOT`XG1l(~e4^{|Id0hZE*=3xHQo z?WzATTi9fmlpjPvw|ga|q0#mNmOb?-RryI^iLJXpW)=ooTC4f*-p>gl>v8s zqi}e{=l`hB*I|N=k<)og+j5o_e|hYK>t>1tt3HFc`n#6)pLwUJ|7YII<6Lk?Xu&_( zY|#>&T>owA-i(ZCy-`G$k1%}T2D-&H0?BXS0g$Z~nI_@~4{LZ?ff!mdgsG>(5Dx;~ zZM-j~lX${*Un@@C@6K7vCUf_qG;fSj>_^U4jbjrW_;4Lkaqbe!iOYWY9qL^CJnnK! zWE1uGp4c7Z%oGsn9ZoGacH$;5DM2T{F%xYUk`3you|gq@TJ2q^oUR$p@Sz}yPMhU^ zG%`f@st>!|?|PHl(|=MeNM{#geJDpWtx;a!3UapEyyla0Ugs;{+9TC^xaITyY`V3V?dNPZ%`8;h+xH-K;Z(u|x^a^ALq#>9vqEBT?<}>yGD|652Je6`W`f zpK#XPc}d)HHTcQ&sAERHsZwd6EGuw{^M%yt(`mX?)Q=&F%Ra9A4Dg5fp#kW9I|Nku zq5Z*-Gz`gvqCgW@-WKKFA zub4%72vgnlo3`Zy&-Uh06J>yUNz3ITbsWs5$)(@ zxbr(HkdiW>%UFW;A`Sq*wzXE}^%RejZz?5&c3q@Tsc>6l0#6~=7m%Nk9_2X0oe@W* zIQOtXs9_a%0t zd+OIKO1{`Y&2@1za$uZcRr}tMOlL(d+afn!O*_53bt;O0HRs}%e7d~m$#BhBWJhA0 zmy1TLTNa@NCFiL0XwG|^4nv7fv^eWO`a=kvw}I8C4b8qN>7S)IOGf+OgV=LhnhC&f z;vIzJ*zq_Prst!wY&px^G@*oQaexuEKm%BoNef(??}v++;uN$>e!6?H?T$*1@~YLg_GJ zgutGTXmXW35&)sDHR`tJ&KeaXNP2NMgG~YNZf(Q}$z_gQOAaoRwkWdvkmfox3Qmn- z$=U4sy0q?oJhf^ImQQlv1D_(;)Wx%vhUfz;vY|x&GKtj&s3|1*) zeO!yr#bMwNMyiMfXG?puaoQA#`n4@EKMkgT|4esRTR)62CrfuFL^{|8i~2nm8#b6p zyZHQ|`cU7kvt^N9wKc9xrA`StIQC=RoV`p7oTi(z&#$uzX%A*S;_{>ZI~KyF1LudwSh?CXswesWNQ9W0J6GxP zqj#E+Bo~$HgyN|83%#u6u1e1QYe{LclwNV^KWAQ;q^Q;wpVRkbfpH>XwwSSG)U;|v z^lMd&Ho`>f*1t@#os82YEzk9|06B6(ykQ6QHl=*mq}wT@lKLi$WHSG7gKYEE*}Bjgj{Ox#3VW^<_0 zhUr-jhWyts*=;gr|4q``?*o@3Z@Us;KM zct{N*#X!E7E&X|Rl2fZ6UoRpLsEma!yA~R{=XB_2Vm&1YnNmE@=hol6dO@R)B%8@; z4WdD-&rq_u=!nN*bt#Beaj&HBMGq;*hsOxYFiDX3XZ=cJp8$k0>1h-8I5=K4_-WDh zdD*nEgps(AdB}H_t9EPT_sF{8flrT|XP0X{W0_tv%}B3hcvMK@u>(U1`uYPWexmiF zR96+206WZ!UCZq*XX8_-BqFihhEn5B%qi~Kd|2j$Uq!J3KJ?iCGiu)Fzy@2$rD~C@ zRkd7wDZ<=LNJ>H#K*yvc7!*2b0$Fv3S&wog0Ix*t`>%I!SO(@MuDPymjmak3uZyrH zw&L%dYSnh5wN!>A9NNMfF;uSX zIe+a(HyseICi^24unWOI&uo6c)I>VZDlD{MBb4n9tG2j=CqE^v(Eh6y`3?rgd7|97 zhc7y*8cN9^V4Vuvh!oBaaNB#UGp96C1cQ z+YCu6#O?hIb;z9Odtx!)ihI>|`$MF0LXAUL!E0NfM**KD;5qw+c+ETvO`^ zJy9m!cYVT>aE33^1u+KQ8!I*9>8rm+A&>nXH*##Cx#};OMEhqqk7lV}LYJifnIhT7&yA)GX}8!JL-Y2-M#D{g(b*#^MXKfn2Fyt(LI)A{p$Y8-Zmhv~ z$43WRP0oq@PgCn1GUutDkRqibYXt;P~m_&f?4Vf+@P zMy02K+F0g{e@WVo^2^H=Q~c>c?z?`5Hj9DbE_8`7Uwr^NCZyDnb~aQ>W6EIGfs|3! zs$A{>_48D2CalA3N-agy9v-@mN6Gz6d-do#;gYM{?J_5hzsa{(O+G(hr-{dJJmk9@ zL3Do$hH2S*%-2e&cERtHzr37AL}`8*AOsR$`vOUz*vKRYVQx7S4TZB>h}fTDH1I2j z!KEd3u6LAiiGhSB+&F(6Qc2+fNNs5~4V4D^2CR=A;o;bdd#ryZVKw9;VEB@@saLh_ zFNQ^8q#^DqB8*y^jpTB@D>0{}UA2Y$WCS14Dl!)m62t|So1<{PSYw{Q8g4|lBMG?l zkKUcM$te27r9<%-;INs;xE~UjGFPLss-n5C-w!ZFw>VGga;1-@!`J!c zghL`TrvOp!KN8DGa=xBhTEotPLZViM67(~E>L2=<@)TBF}VN z%G&ih$QVhFe@LT_Mz8J|&`yhI9!$A4Eh;Wc-y_`fh}|vYgaO({d0G1Mc#EHl_O=}H z69aeU=Iu1Iw0FL_HO|~AL6J6-slz*>xNLE^|apKBGw6k+H~JcMi;^ zSJ=UDNx<_3(ZCLDK$zrYc&v1rW&H;`PZwvxmP%Lp02VtZEm8cwpXH%?X76ew-XG!Z zjr>x{sy(%hm@@;}i2Z(@Y41AyiT;jN8lwh#+`B;(c;Xc}zp1rHwY_C^ad~i*nY4V!BouKCBuYHWJOjSb93cuz07Sg5%M!+ zQbxs?cAotvrUrEVn+mY$Ys zgK?BZRkfn0=+K9?t=lo9IGhYo(bGV9rs1!~n+IVH2Qk6;O5q9(ynhrn{v0!lTSi^1 zyV?%{YBv^@dj(D`s5zrdwRgL#eLi|LQ(8#MQ9@hNk$4W5cU{;J< zn4<3Vs-Ghai;TzfYhc?wrWOZ%?|q6~VaAP(%{$=qtL5~>_v8r!BMrxNfMW=_KRhL& zNt}+Xo=i^?jZkBoa=SERngKOmA4|n!Cs;%oH)DoSxF<0N5_p*I2iLD#N69*6(R?`? zTCPpMwwb`I1RF@u1Ac11dqfb=`lm5yoQF>#zJn7eQ~dv5{<#+)kHI9#~;GU*xoE)+WIacJS=iI#NOM2 z7mxM$Aa;akHT_*mu@>DGbww$azMWBw%xOGSh3;KQ6wl9QY6}VE9Au+-sWk~7-^Qv3 zp8@4p%;S-u>|;^)y{WDFJ#p8Kg_WK-qL07ACpj8VVfp1-cuj0eqdu1xtG}qHJaR5D z;%7dL?oZ}~HCVkzA3<9=O6`*seHV3Bw`aZJ#&8(~7w7vCmY`b6vVIZa>~=+bnw;>g z?O@Sahpq&jWzao0g%&Q(EETJ~ki3>xTS&gF;~0!<6(QDjlPWiN_Y*+*T$0xmh!Px5 z`}13v_;8e~4pNA(Mp-VS$rEw{R;m?1+XAaEdiJx{z<-}lks73SpnSyHE?JEVQThZ7 z_tb}ot@APN+Wz?|z2P>pMKzOtJP*gss=z+HTXWZU2^=syIsIiEm7%Z9f#!gXKVkg~ zC@&5crZ(*pG1Xv(RtZb(*OGs{fr7Jrs)Q>9=|FJ+8P_*L|4XMy*w${uaF2@S1_`o? z#k3wm=?!+?C18TLo>#~spv*x~T!d~xUCPIs-*(LP7V3OR+pZC9&3!5}-_dW3WK;+hY6*7rqhvN;8igE>WMaT6F4HwRiNvt-;5HYOhF`fMUUIY(Avb+0jMa)A22(6|{(qSO6s)>-I! z%b)Y%QBCN_=Y8z!Dba_(!ElzhmR^J-d|?2YVBj#F(C@JX_xquc(EOKemis*?F%It( z+HcJybp^+UzV{2SQ(Z6tv_Hkv^S5rvU|P^%{A8ZGzMQ^=xoBG(uhLg>fbnf@`}Xm* z7WJ*hQeRTYstQVHJql3gzbo+6bW>Cc`hWgF0486%2{AJlD zt^L7>Wm$*~J`GO9&}CN?vjFGK%L#`+eR|+M0LN5uD&#YZFM0u{Gq_7YmxkKG zT(|8|yAWItYmZTwK0>3V{evRn821ObO}19*R{bl~R^u`~GHuN>WScrfX`W>Y0sW0^&npLAS0tTtOz-28~*3g&T)Ch^2*a7iJ~m|BHn^BO-#M5 z`gc3<01aUi58K*|M(O;I#*|+lHOKBYE~gs`?>1cMjzK~j9{qHy{@x<#p^E3$x`|)h z0$^=?Ou$;rch~AREpZlUzi}e%^Sw{PN5O%uLOYpj{V$NvU}7JrIpr4trdnm-<1P!U z2ObhG(s9z^yZ9Oe=BpW6(J7QYTJV-(w$5NmauvwzZ9Y?Rqg`-M_;Uc5eB@&O=U-tk zri19{1mct)e(C1hYkeQPn;O+977D<6M>dz3km$Q0#ne5VhN?v7FRfgOKe2qpnepzbm@1j_+W#aOnxeI8|>Vy_o{LAtKvtr{8hlHe3N{h4{Y*y zGup=rZ+(^DV586RRfVQd>PuNTohjWj=d(p(K5-1lwZyJMzn$cblhok!>xvYV)kd;D zN^3ocAn^i(_huafnwjw*$BD7e+j#p*9GmWlGN4F+a&+{uCPu>|pb@zi%KwEq!bcnXOMhTG|q0Fo@#%^CzZ-c$Dg> z^m}TwzPFz?rL&~Xy(Y6?Fa8Dxgl-~{SDj6!o*FtM;eP3RrovbpBh6N01s?#givf?RoLq{WH*SyeVfjq(ftAgC^~TzK3MVPhNMJ z(-^&d6Qjlz1ZNx6z=+HlzHxOxnpDf zR|x-xe#(CWB(S`6JsI)f&+zgF7rD^+?iVfR08=p7l>J`9EMM z^SdHZ@G%orGs)MdYyEyuim$<0A1wcfkys%K4y?cs6(+$Ie)#hC4nRlTYc=3wiTPNQ zolbCYusBT7!uIE`_h50g=gEFgUZyiG{_v%2<8d{d$hTIe_pGwWTNdg_LPLfYXoTqy8YWK(F1!1lv`0iUt;+)CEt+;7Hov`b@@_qDXp{B3+? zc;rK)541Qk!x6C;C;hz7J-XNsVQnPJ49m#f%`U^?up`FEeH^M_Bh==OvxtEwGMmQO ziGV%Ojci1{B6fA4h|ZA|iIM6Fk)T9-Wm7YXnXB-@UJh)mxIVT1jY40Ga9 zvFerexD{Jzx*hQx>y?vHKLz|#=l$T3G+|r@Et>b7^)lOTcT;UN&}N*d@a#yND=yF|r**mj zTCO_vLYOzNuwO`Uqn6vOn0E??+Iqes$$k@>kZHCXC8E{$)nV@DjqhxMtzS>U)Lc=f zUF~if)(Ue`r_W3Xf4Bb)^v8UATSSx8D*GQYlSg6nk;8mCh67o@)ISNT$7|Rl*JbR0 z0Z4v9-@l!!Fe3E>g@zOp3L(d$cNxaG=Vx_@($eE1*Wr^`i7^4`%=o#M8!pWm`GRNOZKZk zQsU9flkL)~S_GXFq3G{;8&3&6(w&ptQaX!3tx35G(E@KtiNX1H$_g#>uS;e2k5=xJwPYLeNxz(#_Z&g#)gaxcb=9OziVg zA`ce*m3?(1u(AE}!RvP&mB17E_>7uW>#*vmDgbSVR?EJc`4flpT1`!iC8j~So**5O zs85v)f(rCgVYbvQpiA$X%*6UF%Fjuz0S#xf_utqTQ)W?WB2Ojt{TP5j!21q7a_Rfc zz&*z1SCqwNcuDM|3wk0qOd2X-*PG$StshPDL9PW{(D6{$iqPk;g-Mxt!gVZgsIme{F zzZxU~VmpQZagrTZRmxeQWi<#@aFNNCbQ?o~{>rIpNVrS9yh;oVOREZXdT~HpA@p1X zy+r&!xh+!v`;uVhx0C7s#7u}3l>wETZUJ21UG#01#JJn;+#hOnD9fr_Lo zEdJIbMA1iS@Y)%T&cKg3kd%jEyOCnkK`Lv$PoB_Op5zV{_F=n@c1TqP+6E5SX1Ee< zgo2}sn4$9 zr6nP%cR1V1+^toh=$_m!8?c3D>Ww{Y5lt}~JQk?J&Tl`&{MkUc#VL4Rr=D?%03 z?e-*zr$jEn9iN%6dI*zo1eYR)hsvbj_&oUdr<=z-l7W6$G}@KPh1}+w3_p^J&$7Ol zG>W2%Kq{#{lw%^zCf)KBHBcR?K6G`MFa8O*Dt|l}nUs3>EaE;>)0~D==XWknC@Z2J zaQWHa&YSlTHMQ|LWu_&dM~EO7iF}p`91zdiv}K5id5F?Xr8`EfVj-|frZkho0k9ES z5K(;w<$0gbU+M%178R2i!t_61BNS)2GYO(+dH{_dF6@QYES#y1tNg3H{ zPM$w=*V}e)p6>+JoK><$76IZhJMn zZtIricV6g_vB)@{IQ~kO^L<=ipi=6NDCSB9jiu88QO7}3Gb3FLk4O;>vq-=z?j{%TiLy`kTGgk0(V4Ex>4WLz|;mo|`T2xvYSd&GG_JqF~*G?WM|5tQ< z&g!#bnF-w@k6+&#!4geAsU-G(6=qH%xO9gcP0U<6h&~_4#<|H*p=yJnFY@wxc=tvI z@S012>_-w{F{3j46}g&uMl`jZVG2XKjR@ncBM^!uVz-)S(` z1aPi-0{f4TE}JjkmAY+-_2g=Wd`_KpS7f>&!>l(-P_X%FGLc@O4_5slhHf-Zv5?T3 zXJZgEizC4Yil3LJ`C;6WGIe>)5jlA*H{->eO!eyvSaijPqN#Z%_zg16D~>$>^F5mR z#j>Xlwi!HqY71(iKF(^P)?^BG1|jW44#dn*h-OInEo|q7w_FqM2+zzca4ABf6ouR7 zB}`JMF?W;(3I?`I=43pp6%%fQ@2ggmF=}tz&boB)ER$$idN25q|5&=;zeL_U`(z$-2bpw-)*hRr=Hhp($YzI zDNfcB!?$W#N8kGaCcCEH3}S8K)0+Ac>iV{aPsbRsTje9T_XaiX)>~kC^HhlZ=8~KP zj}MEcGIfL;vl4|wh_pJb&|;j*>74rquUx%5dZE&AVq9i!X$;elZ6c!$-%UOH##!G? z4VjA(eD_Y3>SKg5Rb=HuC~t(y)=Yq?)W3$><+j1J2EyNv@k<2;Fjf z&u|=Z>G{-)&H_T-Ub3&P_k!W{+nXwJ4RMdNKs>9?)rgdA7o7-=VXCDwZf7yKk zh(O?h8{>YLdL*5vtWwITi_-W2U6#=*>J`CnPL=KF2b0PM_7ohMmSAyfLGZWrf>QPV0BKdwtEH(DM zM{K!2h|X(2j8zMqVUr^Ovj9=dQSmB_p5wphFN*8%8QcV#;o`v@PXIoU0^>Dv#fea0 zBdDYCUFZXc33>=wkCCQ+Ndjnw;p5iYg!9((L_PILe!lxmHKdJgM_xOTMXvq*P;+ox zc!H#Td}oiLH8-tdhOKI-JL(b>$i}>(;ptHo6k!>K?iYU8AKriIa$7nB8&=iQ1Z4$#+8i^4@?(w$R{i?+~e@8&Zu!d->Zm4}< z%~dWLqw!_;^U*xP#lX{IY+`JQs zR-55*xb-6iaXi%hJ1IH3{NPNL`&DgDz?X0P*tERf#=h2Hh6xLbXgL=)F#fOT{-s(1 z47mpXDc2WCb<8e?t48~ zR1yr#;#nbK$?h*CD7)`YP1pX(*P<3)&V{LN436WX)$T8YG9VV7G9k`?4^n|v!&}vX zi<>I!*o?tnt@brgghw+DuSuol9rSxb8T25m?4Kk(Y_B?<{{jo$S(S8c{VJ5QX9-u< zAUtf^9VsTcHsT++Go)GJrDfXo^L3Q(n~-#<&GW<5xXxVkVsg08%t^8TJbpc`bAUm0 zSSZBv`n22LOqz-HDw*t1^XpU49(UGWnG}KrzdO_*I;P{FK!YN7i9n@^t^pm_Lc^b<45T(DeLP(S_4j&Qh-F-39(meVtrIpOJTSj zo>YoKc7cc=BgVZLm(kvtT3D(w0Qx|qoJ&(w3BzxEl6!Z;cG=$tSX zXOt=2@J_p~;IV~J=YO;3{V$Ujh+4IRk*VfM{g|hT0GoVwO z;OvQ5N{))+H&@y4`sOI|$dc0AS-6=ROn+^DPV}ZP}z`Wcku4BDl z5-KR=0GCC{c5@D;kn0?M-zg0E6&}BnX^z79hz$LeN~;pmSv9tga3(4%K{Pxg z1dzC&cId`q>?q|?9voG)n?zZ>qgZ1~7$sdCI!;0lc~*#Z@NK|BS>NFGz?y10B~l|D)Yh9;-^NM6W|L=r$cL zANczdF zSg|lJB%yk9`6|D^p6d&k3M{cP8H%>3)mWz7ax8Uxn{GqbIX3xszMORnFlBhyY>D5t z3IU!>81s8E!zg?DOLx~=m&yb&B=ASDmRzUX44MBQ+_XM)xfeG(HB0ty>sRvqCTIjk2fYg}sTXG>DT%gVx|iTbVxRoXQ=1=2!Dx~gft&4oru0;Za!;1L2$cecMF-O)r3 zZn+O#Od{J^vGIG)rA}D8>*%cqUa2gRXS+m%M5mitPT5kU`B6%*C08;G#$=_Y#`Xm{ zu>Pf8)jzr>M#>ebZ$CZ-nc{mMl|GUjw4R_M{eLno$k$2C#@H)~B2gF*P88<_Z#ji+ zKmJ?1K9#mg2G)wm{)HCSu<4&6JdqJc2_Dy>PiGfGcr;ox@_fGb&R)b`G|3Dmd6inA z8b9Nz!~2yXjy67|eW;pTI-05EMAz+EOsO)6UaloB;pXC_*U9av4+RDz{6qcO$JoW* ztUT~dY?rsHLcMFmd@&GL_eoxB7WbO=b4-%rw~hcsc;L^C>_OCZBW#*=Kln9n9 zt#nwl(;X8#rhWL?2zUXG+|fJWYMg%|8-!&1oQchfXCcQb`br8s#WrQQcJN!7QM5)6 zgom@q&qyX(5- zW^!4eQ*`;xAe`boS!Ikx$>9O5j7Sf6vuQXkrZ&^sB_F;yf{H6s;f}oje2BcMLPDqE%}bO@Ww-t3W>^86XHOW&tWSG0&N>3}>uh7P($e)a3!l zTZ?6dFN%0)mMWHFkWmT{O%keJ3*ZR_{LdH!E#XROn^l&O3h#?ebs@!S*6LSh{o6tRDq%ZZNUvGhuEWt zp|Aa})~6qU)WX!(Tk-xOk2VEl@hmKB7PN`}yDgH@f87Zx*}vlf5mb0Y>83?^{P~-` z;T{HxO}WpES-)TwRtqE?{<76nS7ENxDAfn3>iWIuSH+@>z~&L zPMFPzruccQmKR`j2rj^Q=5LM76DCYY9IMdYmbXNH2dwJh4GD2KmaAZIYy>$H9;hg( zkQGJV0w0(jM%iLyVsW|9Oo{_juiGa}e4KXV0;zp)Wcqd$VfJr%G9p9$c6(B^hzcZ# z!2tFFvHVy1+(L(^}%ZP zNzO!+>Ku}?15pZLXE_<{?PziY48nm9X`iya`!F8vPJri@l6)$aQB1tBkGpKqKB79V z$}Ic8e{^YjM6Q`7*k_9@9E*tRY0O$#vPR#gX#Tt2>rCNu`q4SB!c?QCKOV|xidH|U z6j0C>7~4rkym!VxErVD$&R*GAGYoB~LS&_E+4;Pd8ZaUJOA$MLBlKnb||Y=SN0-|B$*W)n~7Do9*DtR*SQB zra0^P>~?@jnGF8RJ$VYjZVf4lEBx}hA|l};rYC=2(veW zEaTD(tu`5cf3-Mh{nUrGC$tT*3`1ref|tp+7*yCYaU>%hnZcd3UXRR<{F|_6Gh}3_GVTz+8GQVt>aqY$A-R|B!=`Tx8yUbt69t7ayCz5CG)mI-n&7P#mmlr)okfvwcL zTknuPv$l{&%M2(wr{euebxA4OO^j#{p-2C4)UN99a$P4%ety7TJL17zM64W#He|Z1 zLjPYswkdmsXbqi8xxdrn=@RUmZA?htM2bVU4jo(!K~8mB}<^00DA4P0_=!pOhfY=CcpTV>J7lr6SpkSoVJ(LWl)BPZvV z1`-ub+ptl$6lhXmb|J1@BaKj~9 zKMS{-3Ket{yg|B;UZFWJmFV9g8$ z<-KK-8dRT)VV`3fCjL8_DU$7Jfs?_>=q^cY1@w_Qa6bv#a?*V0V?jteYitYa9@9W4 z;G>t#_)>w=5kuCH+QIiDa$9>@9Cs3exPLKephI1yQm`8EB|OBZv%&%%0;;{a%U?9? z--abgr5$LSnBF}wvB|6L>lUGtO>@eWzk*&8cVnsBF;=4HxR|rMTb6ag0!cK;w3vch zc5+ERpEIX1u0bgn@xV+o>@tz(80#3R0u`7byoqDAq>qrdU>9N~a^+t(C$9geuD_0o z>iyowVLFEH29Z`6O1hL10qJG{NohtJk?xiTkrqTcharR+N@*lVh8Pr(k`yU{-{JNC zJZpXbdCpo4EY6&B_P+MDv>1=baq9jd#7;=Cw>ajZCoAS0t#hv^Y8zQ zMz%(I(%LRS_4gzjIUvvY+le4-4ih;5Tkf-^-lye8^XE5)xYs%JFg^9T{Ki`J)=*!IL3sXmiJ#Dzfac^Lcuf8#5fNmv&i0P}o$g?Bbb)-ngEAgZXGPT+ zTVZdVzo~~boAtb<;1^!~+3nn|K6t2b2Omm&&+g*s$fJz+K=6xmYgX!`)I1f6q@p7E|Dw|t7W+$i^y(&Kb=s`g~blc8troYhTjCqlz;)c{)~ zHS-R}qh+mq;*7?N>R0aQ(CVm!O)LQp{WT(2_Y$w%-@GcKpE9(+M&;@L5htQKoBW}A ztoY-QC;*g^@=hE2RWG4@5h~kV+isTn%&d7y>k_QyV(zcqtM|FDq_5JbcLERCXy+>7 z-uA=%w}hg_TYSVcXN8mteW~sR1F1Co$!#ktpAWvXeCpvx_uDzLzmm`B<_A)aWYh1H zBCgx-Z^kfHUkBs8#bokD$jc}FB8&M^FELV_6KWb|==-yxf=}VYiCVnU4ponmZQ~xk zic2y{OV8W^9QB!dNYktlZEi0CZjfqbSs&FbD%{I--A+{||GhS2T%OJ$RZPN_cKnyd z(GRlG8$@TQvL8FcLqPh}Gf1Ugj?tez%H3j@610*Uxt%uODC=Zxi{{aHMg3 zbMID_mx3?N&gI&G9|-K(DNG=-D3WkarQzsTj5p%QxVZQRpv8`NhU2f-=;y5TYfj}o zPs$jSKX&GWgB2kY*_Z$ojLZve>)F@W69=EYt1+09)_XRJ|U+sY2Iefg&RB) zc%WUbr;zB*!IcOAs>~ca{=mJQ=$`|8oXZ029NVve6pQY9#i|qKU_T-d3^QaMPe}Jp z=JulC!e@ELKE(agCGVZz5!CzB4X>VIC3684w-qmqn8;v`?HeVeEvchwaG*40-}2MF zZTz;hLGaE$Kn2E@;-NkAnXeFs#B3SWZ?si0T>Tp&Ko3D7 zyuv|(=A<psaPdd2MXO4w_m$vw7hab^s=<10*FXOf zCQ*r9yI{w+z%0oJy=t2%{Nuctin|)9buN4q*qyj5e|UKsrQf$4^={X8SWI|3n4+=e zqmn^m`ua7Jz3}U78S{l5KEa8597s= zX2&L+u7B5g^@xvN@TvX>v$8HNq7jAH%;lg{{%S8Xm>?F*Aw_l&DT+HYgV@C=?@xMI zh!Fjpj>t04m@b?wE=hplA1&I$c3$k49C~+cUEu3T@MD*I0wn4;v-V{dwQrlX1&m(E zVO;&o99uw!9Eoac}b^(ArC%L|G z>j@=_9L~&(R?d?Zf63_C6@`0mDZ%eu>+?G%af=R0CaArq&iV5>4fXAkQE0wOSs{_j zr=r+Vw&c;!VMK_eSWZ)@d#fZPF2y+T zB>gX0MxC9;%=VeAJUC+b469PBM420zG#*5zD`o4!a6P9_+HloH^QdV|z+#I`tm1uE zpsR($t|CGOa_apX)19(B1BD`w(sE@#3Pbny7y-v3J30LFi_{0R<>zsc)01LsddwOl%Gn{= z`kbH>%2Q>zC)NePqMW%1&xl;wGUrnNry2WH9v1Ist30S|??(gri;5MO*v-I`2{K0X zcLgWGIxzeXc@-m?<%z*@WK{cW#t?A|TfOK;0 z9zoWBkt}8>B)R>;wd0q^2M*b_{W)WxsR&Q@T_E&D{#4 zFf8%T>VV2UYA|o+?|a&*)BiB;QtwZ1Xgno%+$2Mh9yq6to+#?SO~d}3W4JLa8Y6w` zdo5YCpwpCppTe89;PGc6#2b|5t^idxag$dtnA45sXJ@Ha9Mq^}r|B3VU_Ke7{_NPp+eVu~7$N(d4uEhu{7 z)rbw)Q_DCoc9k@nyAmbo&Qy0K?_MJd%RXR@&rx^)!Ro&-O8+@KnmQ;XqOApB~m4a{T>*(#p^&uLnX z?uw9%zuiIORK9mR_#yPT^im9!piFD0;<%zGIa3Pi92Yx)tmvotkbSX8DqcH z#XW6JwpWIe1;+@iwh8pN&}j30r?(wW2EEb!^h=-goLyt2ACK_vzY{TgV#)lxcjMc` zu?Hc0#LU1NKc-RmMy$0H^_;dK$0$zKH}`6)0N4Jhb`>&yYX+>FNQk- z%aFe~nz7`Midp)rpQ#^{l4pb>zu#A4LHlflfSz}$V#0IvUyj|7cVIo}n!fsgf&6lv znU`w|?nc#?0wUC_ZlDzX{j{BmCnbH|J-)bu73Tu9_<&L1$__2j%!8_eDkzrLF2xIS(;xOGFX3`=qi>9;+~e&Z z!?kQ%>O!QQPhr9Q&Y3uKQHY)*uMbNhL-G76Wr)hhevFA$xtxpBRc%H#Q$+2hwkBh% zERVDY*0P5Z-ux zQ6Q|}``eb9WmQKKF6vE(vI#x(R_8lDpz#@`6{nSH5uxYV*h$R^n9(CJccmp`0ogX0KYA}{fi}`T@%i(Vba8R4-i}-0o2(CgNMQcEEsm3}v{#5k0V{PW7+qkq(nI`O$G&tiEx@hRI$ zE)F=wi=~mE)Rou9Pun?P#o~>~YQtVD(Qn88#S*9Voc(lt$PlcQ#-h+<9~Dv4T59Xn z_;TTBY;gHnF;%&(isFzLYqlG) zz@ef_U^t-XBFV&(;wgUoRnM07^PxzC-uwqei$};?W~*MDs!c{@xZR(V7-wjVvzC6&LrKvF{h;{N&sf)Jm7gOrW;}@5Eap zLs{F-L#xJB6_02ZA7&>{Y~Ue%XT6_q#v5*|O3+h{Eb(E&EX=U*pHR^W-_$729|=#r zuFof(9lbSjw97a@#Ww5d?tZ|$a@2rZ+ws~-t_;69ACS8BV6pIYemu}4LPj>qsq;AV z9+=@!sOpHVUFP-fm#~|HgBwT2r2!6A3f24%f!o&kj$e)k5CJ@3xBt^ zZl)U75R|}1P$q?L{Y$~dv_m$6yzcEv0f5;@#kV13oX)p57u*zqXh9IM5*;`q(YlXeO-TzQA z!a}ENn>0ewv#LLX9b;{v`&L~)|E#WCL^r?uM^z)ROdLcUHh@{Es-=jrs!%b^VGC z7Gj5{`1(4m<8qzuHf01HLG19BlJj4*Gg-Xm^fSB<1s>G=yuUMu$%`~&dtNBBQ7n@2 z&gZsQ`B+tV>p!7Qfl3h2zJ8|ggjsDPCTN6}?rBU2<3%R%bV}ZztNM8A4+HwP9D^J9 zvy)=a_<08Y_S--pxBslH6Ihef?HyRSmfu2DzQ1c;57DutS$jPQO2sz#D54B6+0%AV zQirR2c=!#B_5OUo>h`?X+{%CBw&;!eTeLH!(&xK16;-##Mj1N)TT&5q$<>A)8j1bE zr`ZVvmRFj3y44DI4K`9hWzGN_OQy7Th^P4<2#*yAkra^!xHsJLG2N<*VUMYvBz*3VvT9if@N5g-J6bGQ;xg_8 zj*o@mX--u4-jzKV{h?^D_4I)K^Xfx2n(R1=gVp+jm46*UXAHgH%An?*+3wkaGOK&+ zDW~#h3f5%D3hR6Y0!yr-7SqN*)V5dD2U-|bLvK%ZHfoAJdxK8V`%DyR8SN(V>iUb# zz4yWhhjNZ7k@fAtBoaASBQb=3Eo3jB>YOWh?z@56!&c30>VI`M2x^(|aq2Vo@y`GvsTabjjCJjh(4W7F+j1ux_7P_T>27Aa zvqh?fKJhDx_Q@Uet&D$>>~qC!MQuBRkYCcq@H#{b^cJhnxY$iK^k|Y240^DUV=(s5 z|7`O$4b`M=(S@nVB-P1F$_~Ow2hLxG$K*i&EFY1&&Jnu0)A@IzLl6=RB8t}eZ2t7D z^82g&L8+008lUVwFwUhVEl?d1tEZlV6_pjIdQSKMg)tY~tA=g|fTaAzNZ!))dNi}1 zJRK64rUfUpQ+w_YQ#mH}4a%(-dJPuR+(68K%1-{8rTrW-#ONc?N?8fzm^2pp1^L39 z%7pFpo=Ff*3w{b>s7T0gT(eV{{D=2)IVns?qq%zBg7ZUx539_fN2U+GliRwYJm0T6 zgq#oA0&R7@>od1;MteoR2kBLv)jY0bJT>a6w68Q7eBSI1b~DTT@`;OwbHuLw+*f>_ zL+Kd~(X->1$l@H)A8w_t+xgjYZVq(5f7md-G1a58JH&W0E5n_xvMaRDd652_{|ToB zIgfFzam25WP$i^-mJ^ATvSxcm9*Fj5sq-^>+{wZB2ay3rG81WQv)91IwE4mrk^sV99ZChouAIs^8?D&OK7*RH! zjvG_u5TuRikVzFK?PuOM^KON5(dU9UjiD3=UihmIk*sU7lXMCSz+*aIC2K%++lqH( zo~|9S#>8i=chk926w4@H=|{6PRr&g?L&oP66$@|=W=AYB=0lA3n?92F@e zW2GCo7rDMyy4yco8uwgTf8RIG>i7}I(R{ddBq;3R?6T3ysAzDq=%&^hZAaB-{ti|d zQ#&J>r0icytBHY+*3`8>)OfSj=;3TIgK~o{kpUXjZ=Ox~)dR$NH-q1->&HGX4om=X z=jjg{66Y*j2+~oZrIxO&R@Pbj$T-Dli>Y}@jK__I|6+Wk4|Qa`SE`xplVv!{yQ?N7 zXj!zHBy^EZ9x8ql!S}u27_F&~XqoqapK?9^~)$MZj$)ZDoV;!r|a^M?C?GQSj6$c7@ zMJc3A10ZS#`FU~5aavwetXGZvFUa_-;_5XEF;#b)SEVM+c~CAVEvXZkV=_)i{hE0` zAEaO6Lp6(ivN<`uMNEdxTX&xUi-{nGg~s~L2VY*vZPRX-^`&g(fG70d$(Y{XVEzl6 z_}VdIDFKL(8-pL$SFQRr-H%@MKtIThwF9`jP6n7H%W=`rl-b{_OUoUX04QXTL<*VSqKybY>T-2shC`a}Sz|jqJRfF6d4@f4|Qjp&skgHf= z;ocm*t};Af&@EXsw!{A$udXH>SmjCqB%s)6p2&x9C7^BGRZZRqXbKurvZ^`tr!#^<`ePo?KoCDBlDDkBmo;%$Nj1Tq8{E zT5IHPi6cdbPQ?uIIBtxfE^u?YI^pZ)ulNEo)g~Q4>35fID&e_ zJR(~L#T!xcSdJd9(sUaBu<1FL={-40G*ih2>Cy+vIX!~?GK7b>j0}$Fnzy$iTxN8* z1M{HjlDcGQ1l#nvpj*9^~zr->{0)k`vSD*|4nd4pgaob5DsuHt{NMk@> z@oN0~$m)i@ZI~n+RNWJ6w~Ku?)l@-F3&E41FJrF8hq@_Hx2i(v9LSai?tm%8Ut$Gx z3fLYUwsn6=7~td_P$&VNQ8rSkV}jtDjUi4SDZ!~{%|4;(DIpR6uOwQHY#dAdh`;WS zaHRxPC5*T*AR`qf_$a<<-;P!!vl};nB@+guXTK3%^LQ8E^oc|kmV}8G=6+?9cT-CE z!?zwxmI*seIvfX$2%*j}IWBmnJasD$6u-Fw&_0&BD%OJe|FCGh!@?D7X>Vg40;O~Q zg4MpgZGwDcBH{kC@zR0{4r(-`g5%n}Uc*0=hMh!^pvbZRXFn}GhDJaenvx-l`*0Z) z0fKJI=|vYIgXr^5k}!OX2@*zXj%!-N?0OH5vp9sL`wIKdbZSui>Qe81EWN-&b<2W|Z9XA`d z9;c20f^TB;9h3S;`fa!*E?NN|?@~KdiMBvJfac^7?H>T7o5KO#90R~44hJ<8c-plg z)QoV6_3VXMArUn>k-wKPfgNRcn3^UxK4Uye= znNcz%1|3BJgAzC1!J} z*W->m04V|fm`3>X!^R_PN_N~hQ3fEm6c`(FNk$5h2p%K}Z|f0Qz5B;xZVP0B|98+=$>frb77WQVjiX9qSoiIbPV8 zS93ea>qeY?34BbCCU$OZE$;rOq=t*LH&CisvpZJ^@CN9U67?{Akk+|?0Pr2e^IE#Q zlM+pUOEbCi)LQ(q3P4X{FInPXtlwjU^S5ww6@&uh}v7eR-x1GNI?5|8Ry-&i+q%Fe6&Gcu}k zJBxi5r2v=0g6Cqx8OE@|F&LoSBLT~Owt$|Yz<5j3+x}o^nr)Z=ojz>{KnR}4DH1qv z95g93PLOXB1de1LrPy~srQ+9zKjI;FaaDkatz)0XX0`TD>or;A$+Dq!3)wXC=%dh&l>Sau`|UL+G4AVE~}v>El(hY8j;fH|#% zXB4lYUi-*vtnlDrrwr>3FfKR1*kS|V=r3m?2(kQIZ!d<|_lKz?>@`Q9DS@*M)g z^aKHaHW8jl0WyozLdcdW;eS{t;SGxsHCRdG*!AJ_2xFSY%l`r-;yM4z{#k0k;XK$O z_!e~NMa*SxCJzwt?ZYjTJp=&$S%4Zn8@m7f=|u!*5kp>&(nef`Vx1KhjBm05KOW?8 z;OW;AfMeDZgRhlwmNFS2uhlWtb0Pq|Q8*0xWR7>7&MUKjbz zwwl4*8o>k$A=_#rxs#ch4o-B?XMq-@q2r&D>aQsx_8#0Z{LZx@xD0Mscbo_KALa4z z|A%A}jw!Yz%i`6Uhh#yT^qJQ(+er7_O^J{jFQG?5hUe@X_n}Nycncx)xk#RE8=vFf z2UqVN`x`*9Epeqpfdn;;z;}-e4<<$91A)#;6mBe~4P*wu(N&?DAJLo`Shxo@{OOdz zweRTYumDS?avI)|mT+KPGXYqbj{+xG8QShuu!H0|3Ok)uTln`8U;vo|#>w6dQ%?hl z_??JkYqotLySy1$1K1643sa@m*`S5xo21arjg58GwWUXYR+oy*UsBI$Dg27aq%?w~ z7JyS;NQI!AE=9o-2`DZToB*7Y2qZaBZ4X`*kO9WAO(W(^je(qF?9PBykMO?_WQHDE z;G?<~)~{~lCQoOC6v+q9ynG%ay}8O> zrMycNq)X3`|L6bQcX=C6VxAhw=F&-r7;5^Rn-SDGxIDc7^Ks9{`t(&hTqTGS-jia8 zzJg!tV)d4Sg;)UM>@Ok}TmUkf-ANS}X&HSdZmT|&r&R%V+X;|oDc|!|K=T#ZlhO@` z?l3vpC3~O>@%ot+VBT>X^?CmeyW9vCx3&KqR2zsH2%t=&2i1qq<>Gi?!>&WnmW74G zfd});f`eU|%ff{L#l0I>?XBgM;F|$?S`%{79a$SuphpfUO5mv+3e7xN zt;RfnyN=`6KZX%mT4)Y!&xib_|7aNiC?11<&Ia!o9gae5e2F4jx|SkbYxce+44p|J zS-nb&!`^>R;OPgY0?MxXh#E&sJu$>go!5Q&3Ks3LCsVH%nMdL znja0lmo5Qy1U2SG)HvKlM~S%E|mfc-Fn^c|G`W;&z$P_9E}2g|%|!@>c=W zv4NOUz;4gheZNjNN11#I4)`a-`m1=#1GCKv$3^ewO3$Zs>Hf?Nc1;BLTOKGKAgB=EesMzhtoU%$WqBG`f0bfVCS$)d=4 zo6LYzNa0eYEB&HKNEZf%`L51~yln2$Ph2gKcjz1^$R#IR&Jf7;7yLObcKxX7bMfwz zTpjXjRb{>;9p*xL3+9qyi>(jS!xhC&TdFg}A8B+sutF$lL-=V!9@B=Hv4(~JnW4U( zjsBK_uZA9LLcjOa-6J1uu8oOV5EVLm9lxNN*iaFevf+}=XF`zub5{~KX{NTzSDSw7 zSX4zzXR2JO`$=MMI+bOQ*Qjs@qQYS*KRFG@1>=fJB>-`7fc6XsdXYdv4O=Q>BL-S+Q_ObCc1vHj@6` zA{Lzw7R;5T)hsr>OlAS^)SNG@g`NP9_kne@|2WrvVD`sd|itifgf{^ex3F!4&dmT%Ak>-0~D+)IwlYpm|y0OIrDfW zAamM*{)SNVU1l4FM`)@nEURvSg-;Z$WSRcG2N!Y|`CqKmlL)W=^gHsyc8X@QBm4RO zoFhWGs9tEad7jz&Or+a$b6~+GHXBfVCOvSF_wc zSmD3{Bl0|=!dZ~o=>)u!b_R%gMN!K`z$>+7^g?9{Q5R_dV-to7Zy#JYogbXLXs6U=l( zFG|n1Eyc)}dduOET3om5jaTm4MupQ*t`3YuB!u!$1Qa~P7*-l8(J6_k z@w+=Dki`0~bWm;{mG$Lz^qugsP0_-- zJwzg0V@HI8{2$lLJj`<(H{uqIZT2PJ={uS+nRX5t?q`T{jk1&lmid*=Rayi{d1s;k zG~jXu;&6ej&NR}a5UYSizuf#+KPqTO&X3oVu*CaV!}OB9J^z*7K_S-dw7G6wY-Pk` zQuJb*Iqhrb4B!+rt=(_-blTw2d;99@?rjwg129>*Z#3fhlZ6l#y3$>+=t=7?SbT=~ zCjbLCKuckqF(UB+)-W?tpOkZsIKY65eY?w3OK!~t!ONR&GCwts07hmIzDb)ebSAd& zsNUdlIQv%2+f;!YmTBO7vBB6qkjzpI2@Lx5z3?@$4cQCzJkHJYesB8{fN`}heY=eX z!Q-2|@T3|NLEYwL!3)IHvFhlV>++|0D`hZ}o~$+$SoF+dyh^M#xrU%NR#3N<$YaE)AnLauhWttHN2>r8 zI$`my!%jotAEN+Rq@rIqX#3m&=*MZSbC=H-#}|Ca%*4V451;IMCVC&tRoabWbb6AL z32!Q8wqkSgUM>2qov!TyfOH)Zu9w|i*_It#=VT<))8mG?-@K?=^CSt?*_CeCSV#(y zJ(MCZ9X&j@;dfk|I~*Kdz|W)FWVB~+6uP;0r}^cY1fx#_dS7CBa1P%@Wu8z-r;QmV zF#0t!N{jGjlex3UMT)?S2mlONk`)~~R{^3<^CEG>+f>T&~0LVTcWcc{P<38+8ONxfI_!^R0itn6L zU~-7!p>RoQ(qZf&88&O5w8X!+clTObYm=1dhIK96GB#ICfvGLe5WU(0J3zxK}Rpw=3tLt$2sE(C<=Sc`K7&diZCAbCm&87(5&{zUL`(H5bW4x{c2 zvq{qMt)rBxbZ?77UbJE@?hYCusqUd%r>8$6_$B~)HjBuq%O9@aNtVM#!x}h!r^RXe V*Ppl8Hi;(v?^jgV{@+>5{|{p6e{lc+ literal 0 HcmV?d00001 diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/Assets/license.md b/src/Cortex.Mediator.Behaviors.FluentValidation/Assets/license.md new file mode 100644 index 0000000..3c845d4 --- /dev/null +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/Assets/license.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2025 Buildersoft + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/Cortex.Mediator.Behaviors.FluentValidation.csproj b/src/Cortex.Mediator.Behaviors.FluentValidation/Cortex.Mediator.Behaviors.FluentValidation.csproj new file mode 100644 index 0000000..f367543 --- /dev/null +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/Cortex.Mediator.Behaviors.FluentValidation.csproj @@ -0,0 +1,72 @@ + + + + net9;net8;net7;netstandard2.1 + + 1.0.0 + 1.0.0 + Buildersoft Cortex Framework + Buildersoft + 12 + Buildersoft,EnesHoxha + Copyright © Buildersoft 2025 + + + Buildersoft Cortex Mediator is a library for .NET applications that implements the mediator pattern. It helps to reduce dependencies between objects by allowing in-process messaging without direct communication. Instead, objects communicate through Cortex Mediator, making them less coupled and more maintainable.. + + + + https://github.com/buildersoftio/cortex + cortex vortex mediator eda cqrs streaming + + 1.0.0 + license.md + andyX.png + Cortex.Mediator.Behaviors.FluentValidation + True + True + True + + Just as the Cortex in our brains handles complex processing efficiently, Cortex Data Framework brings brainpower to your data management! + https://buildersoft.io/ + Cortex Mediator Behaviors FluentValidation + README.md + + + + + + + + + + True + \ + Always + + + + + True + + + + True + + + + + + + + + + + + + + + + + + diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/README.md b/src/Cortex.Mediator.Behaviors.FluentValidation/README.md new file mode 100644 index 0000000..eeb265f --- /dev/null +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/README.md @@ -0,0 +1,181 @@ +# Cortex.Mediator 🧠 + +**Cortex.Mediator** is a lightweight and extensible implementation of the Mediator pattern for .NET applications, designed to power clean, modular architectures like **Vertical Slice Architecture** and **CQRS**. + + +Built as part of the [Cortex Data Framework](https://github.com/buildersoftio/cortex), this library simplifies command and query handling with built-in support for: + + +- ✅ Commands & Queries +- ✅ Notifications (Events) +- ✅ Pipeline Behaviors +- ✅ FluentValidation +- ✅ Logging + +--- + +[![GitHub License](https://img.shields.io/github/license/buildersoftio/cortex)](https://github.com/buildersoftio/cortex/blob/master/LICENSE) +[![NuGet Version](https://img.shields.io/nuget/v/Cortex.Mediator?label=Cortex.Mediator)](https://www.nuget.org/packages/Cortex.Mediator) +[![GitHub contributors](https://img.shields.io/github/contributors/buildersoftio/cortex)](https://github.com/buildersoftio/cortex) +[![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) + + +## 🚀 Getting Started + +### Install via NuGet + +```bash +dotnet add package Cortex.Mediator +``` + +## 🛠️ Setup +In `Program.cs` or `Startup.cs`: +```csharp +builder.Services.AddCortexMediator( + builder.Configuration, + new[] { typeof(Program) }, // Assemblies to scan for handlers + options => options.AddDefaultBehaviors() // Logging +); +``` + +## 📦 Folder Structure Example (Vertical Slice) +```bash +Features/ + CreateUser/ + CreateUserCommand.cs + CreateUserCommandHandler.cs + CreateUserValidator.cs + CreateUserEndpoint.cs +``` + +## ✏️ Defining a Command + +```csharp +public class CreateUserCommand : ICommand +{ + public string UserName { get; set; } + public string Email { get; set; } +} +``` + +### Handler +```csharp +public class CreateUserCommandHandler : ICommandHandler +{ + public async Task Handle(CreateUserCommand command, CancellationToken cancellationToken) + { + // Logic here + } +} +``` + +### Validator (Optional, via FluentValidation) - Coming in the next release v1.8 +```csharp +public class CreateUserValidator : AbstractValidator +{ + public CreateUserValidator() + { + RuleFor(x => x.UserName).NotEmpty(); + RuleFor(x => x.Email).NotEmpty().EmailAddress(); + } +} +``` + +--- + +## 🔍 Defining a Query + +```csharp +public class GetUserQuery : IQuery +{ + public int UserId { get; set; } +} +``` +```csharp +public class GetUserQueryHandler : IQueryHandler +{ + public async Task Handle(GetUserQuery query, CancellationToken cancellationToken) + { + return new GetUserResponse { UserId = query.UserId, UserName = "Andy" }; + } +} + +``` + +## 📢 Notifications (Events) + +```csharp +public class UserCreatedNotification : INotification +{ + public string UserName { get; set; } +} + +public class SendWelcomeEmailHandler : INotificationHandler +{ + public async Task Handle(UserCreatedNotification notification, CancellationToken cancellationToken) + { + // Send email... + } +} +``` +```csharp +await mediator.PublishAsync(new UserCreatedNotification { UserName = "Andy" }); +``` + +## 🔧 Pipeline Behaviors (Built-in) +Out of the box, Cortex.Mediator supports: + +- `ValidationCommandBehavior` - Coming in the next release v1.8 +- `LoggingCommandBehavior` + +You can also register custom behaviors: +```csharp +options.AddOpenCommandPipelineBehavior(typeof(MyCustomBehavior<>)); +``` + +## 💬 Contributing +We welcome contributions from the community! Whether it's reporting bugs, suggesting features, or submitting pull requests, your involvement helps improve Cortex for everyone. + +### 💬 How to Contribute +1. **Fork the Repository** +2. **Create a Feature Branch** +```bash +git checkout -b feature/YourFeature +``` +3. **Commit Your Changes** +```bash +git commit -m "Add your feature" +``` +4. **Push to Your Fork** +```bash +git push origin feature/YourFeature +``` +5. **Open a Pull Request** + +Describe your changes and submit the pull request for review. + +## 📄 License +This project is licensed under the MIT License. + +## 📚 Sponsorship +Cortex is an open-source project maintained by BuilderSoft. Your support helps us continue developing and improving Cortex. Consider sponsoring us to contribute to the future of resilient streaming platforms. + +### How to Sponsor +* **Financial Contributions**: Support us through [GitHub Sponsors](https://github.com/sponsors/buildersoftio) or other preferred platforms. +* **Corporate Sponsorship**: If your organization is interested in sponsoring Cortex, please contact us directly. + +Contact Us: cortex@buildersoft.io + + +## Contact +We'd love to hear from you! Whether you have questions, feedback, or need support, feel free to reach out. + +- Email: support@buildersoft.io +- Website: https://buildersoft.io +- GitHub Issues: [Cortex Data Framework Issues](https://github.com/buildersoftio/cortex/issues) +- Join our Discord Community: [![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) + + +Thank you for using Cortex Data Framework! We hope it empowers you to build scalable and efficient data processing pipelines effortlessly. + +Built with ❤️ by the Buildersoft team. diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationCommandBehavior.cs b/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationCommandBehavior.cs new file mode 100644 index 0000000..b9eb2e3 --- /dev/null +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationCommandBehavior.cs @@ -0,0 +1,43 @@ +using Cortex.Mediator.Commands; +using FluentValidation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Cortex.Mediator.Behaviors +{ + /// + /// Pipeline behavior for validation command execution. + /// + public sealed class ValidationCommandBehavior : ICommandPipelineBehavior + where TCommand : ICommand + { + private readonly IEnumerable> _validators; + + + public async Task Handle(TCommand command, CommandHandlerDelegate next, CancellationToken cancellationToken) + { + var context = new ValidationContext(command); + var failures = _validators + .Select(v => v.Validate(context)) + .SelectMany(r => r.Errors) + .Where(f => f != null) + .ToList(); + + if (failures.Count() > 0) + { + var errors = failures + .GroupBy(f => f.PropertyName) + .ToDictionary( + g => g.Key, + g => g.Select(f => f.ErrorMessage).ToArray()); + + throw new Exceptions.ValidationException(errors); + } + + return await next(); + } + } +} diff --git a/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationQueryBehavior.cs b/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationQueryBehavior.cs new file mode 100644 index 0000000..b48c633 --- /dev/null +++ b/src/Cortex.Mediator.Behaviors.FluentValidation/ValidationQueryBehavior.cs @@ -0,0 +1,40 @@ +using Cortex.Mediator.Queries; +using FluentValidation; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Cortex.Mediator.Behaviors.FluentValidation +{ + public sealed class ValidationQueryBehavior : IQueryPipelineBehavior + where TQuery : IQuery + { + + private readonly IEnumerable> _validators; + + + public async Task Handle(TQuery query, QueryHandlerDelegate next, CancellationToken cancellationToken) + { + var context = new ValidationContext(query); + var failures = _validators + .Select(v => v.Validate(context)) + .SelectMany(r => r.Errors) + .Where(f => f != null) + .ToList(); + + if (failures.Count() > 0) + { + var errors = failures + .GroupBy(f => f.PropertyName) + .ToDictionary( + g => g.Key, + g => g.Select(f => f.ErrorMessage).ToArray()); + + throw new Exceptions.ValidationException(errors); + } + + return await next(); + } + } +} From 94f4c953f2d834d361b2d1e7881ed7aaa6276cb7 Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Mon, 28 Jul 2025 13:38:53 +0200 Subject: [PATCH 2/8] v2/feature/[Add support for Keys in Kafka Stream] #127: Update Kafka integration and versioning for net8.0 - Updated `Cortex.Streams.Kafka.csproj` to target `net8.0` and increment version to `2.0.0`. - Made `KafkaSinkOperator` and `KafkaSourceOperator` classes sealed. - Reformatted constructor parameters in `KafkaSourceOperator` for readability. - Enhanced `Stop` method in `KafkaSourceOperator` with null checks and improved exception handling. - Added `KafkaSinkOperator` for producing messages with keys and values. - Introduced `KafkaSourceOperator` for consuming messages with keys and values. - Both new classes include methods for processing and managing Kafka message flow. --- .../Cortex.Streams.Kafka.csproj | 12 +-- .../KafkaKeyValueSinkOperator.cs | 65 +++++++++++++ .../KafkaKeyValueSourceOperator.cs | 97 +++++++++++++++++++ src/Cortex.Streams.Kafka/KafkaSinkOperator.cs | 2 +- .../KafkaSourceOperator.cs | 24 ++++- 5 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 src/Cortex.Streams.Kafka/KafkaKeyValueSinkOperator.cs create mode 100644 src/Cortex.Streams.Kafka/KafkaKeyValueSourceOperator.cs diff --git a/src/Cortex.Streams.Kafka/Cortex.Streams.Kafka.csproj b/src/Cortex.Streams.Kafka/Cortex.Streams.Kafka.csproj index bd11617..d3ddb2f 100644 --- a/src/Cortex.Streams.Kafka/Cortex.Streams.Kafka.csproj +++ b/src/Cortex.Streams.Kafka/Cortex.Streams.Kafka.csproj @@ -4,8 +4,8 @@ net8.0 enable - 1.0.1 - 1.0.1 + 2.0.0 + 2.0.0 Buildersoft Cortex Framework Buildersoft Buildersoft,EnesHoxha @@ -16,7 +16,7 @@ https://github.com/buildersoftio/cortex cortex vortex eda streaming distributed streams states kafka pulsar rocksdb - 1.0.1 + 2.0.0 license.md cortex.png Cortex.Streams.Kafka @@ -52,9 +52,9 @@ - - - + + + diff --git a/src/Cortex.Streams.Kafka/KafkaKeyValueSinkOperator.cs b/src/Cortex.Streams.Kafka/KafkaKeyValueSinkOperator.cs new file mode 100644 index 0000000..be127d6 --- /dev/null +++ b/src/Cortex.Streams.Kafka/KafkaKeyValueSinkOperator.cs @@ -0,0 +1,65 @@ +using Confluent.Kafka; +using Cortex.Streams.Kafka.Serializers; +using Cortex.Streams.Operators; +using System; +using System.Collections.Generic; + +namespace Cortex.Streams.Kafka +{ + /// + /// Kafka sink that accepts KeyValuePair so message keys are produced. + /// + public sealed class KafkaSinkOperator : ISinkOperator> + { + private readonly string _bootstrapServers; + private readonly string _topic; + private readonly IProducer _producer; + + public KafkaSinkOperator( + string bootstrapServers, + string topic, + ProducerConfig config = null, + ISerializer keySerializer = null, + ISerializer valueSerializer = null) + { + _bootstrapServers = bootstrapServers ?? throw new ArgumentNullException(nameof(bootstrapServers)); + _topic = topic ?? throw new ArgumentNullException(nameof(topic)); + + var producerConfig = config ?? new ProducerConfig + { + BootstrapServers = _bootstrapServers + }; + + keySerializer ??= new DefaultJsonSerializer(); + valueSerializer ??= new DefaultJsonSerializer(); + + _producer = new ProducerBuilder(producerConfig) + .SetKeySerializer(keySerializer) + .SetValueSerializer(valueSerializer) + .Build(); + } + + public void Process(KeyValuePair input) + { + var msg = new Message { Key = input.Key, Value = input.Value }; + _producer.Produce(_topic, msg, deliveryReport => + { + if (deliveryReport.Error.IsError) + { + Console.WriteLine($"Delivery Error: {deliveryReport.Error.Reason}"); + } + }); + } + + public void Start() + { + // no-op + } + + public void Stop() + { + _producer.Flush(TimeSpan.FromSeconds(10)); + _producer.Dispose(); + } + } +} diff --git a/src/Cortex.Streams.Kafka/KafkaKeyValueSourceOperator.cs b/src/Cortex.Streams.Kafka/KafkaKeyValueSourceOperator.cs new file mode 100644 index 0000000..ee33f96 --- /dev/null +++ b/src/Cortex.Streams.Kafka/KafkaKeyValueSourceOperator.cs @@ -0,0 +1,97 @@ +using Confluent.Kafka; +using Cortex.Streams.Kafka.Deserializers; +using Cortex.Streams.Operators; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Cortex.Streams.Kafka +{ + /// + /// Kafka source that emits KeyValuePair so the pipeline can use message keys. + /// + public sealed class KafkaSourceOperator : ISourceOperator> + { + private readonly string _bootstrapServers; + private readonly string _topic; + private readonly IConsumer _consumer; + private CancellationTokenSource _cts; + private Task _consumeTask; + + + public KafkaSourceOperator(string bootstrapServers, + string topic, + ConsumerConfig config = null, + IDeserializer keyDeserializer = null, + IDeserializer valueDeserializer = null) + { + _bootstrapServers = bootstrapServers ?? throw new ArgumentNullException(nameof(bootstrapServers)); + _topic = topic ?? throw new ArgumentNullException(nameof(topic)); + + var consumerConfig = config ?? new ConsumerConfig + { + BootstrapServers = _bootstrapServers, + GroupId = Guid.NewGuid().ToString(), + AutoOffsetReset = AutoOffsetReset.Earliest, + EnableAutoCommit = true, + }; + + keyDeserializer ??= new DefaultJsonDeserializer(); + valueDeserializer ??= new DefaultJsonDeserializer(); + + _consumer = new ConsumerBuilder(consumerConfig) + .SetKeyDeserializer(keyDeserializer) + .SetValueDeserializer(valueDeserializer) + .Build(); + } + + + public void Start(Action> emit) + { + if (emit == null) throw new ArgumentNullException(nameof(emit)); + + _cts = new CancellationTokenSource(); + _consumer.Subscribe(_topic); + + _consumeTask = Task.Run(() => + { + try + { + while (!_cts.Token.IsCancellationRequested) + { + var result = _consumer.Consume(_cts.Token); + emit(new KeyValuePair(result.Message.Key, result.Message.Value)); + } + } + catch (OperationCanceledException) + { + // shutting down - consume loop canceled + } + finally + { + _consumer.Close(); + } + }, _cts.Token); + } + + public void Stop() + { + if (_cts == null) + return; + + _cts.Cancel(); + try + { + _consumeTask?.Wait(); + } + catch + { + /* swallow aggregate canceled */ + } + + _consumer.Dispose(); + _cts.Dispose(); + } + } +} diff --git a/src/Cortex.Streams.Kafka/KafkaSinkOperator.cs b/src/Cortex.Streams.Kafka/KafkaSinkOperator.cs index abe93af..00608fc 100644 --- a/src/Cortex.Streams.Kafka/KafkaSinkOperator.cs +++ b/src/Cortex.Streams.Kafka/KafkaSinkOperator.cs @@ -5,7 +5,7 @@ namespace Cortex.Streams.Kafka { - public class KafkaSinkOperator : ISinkOperator + public sealed class KafkaSinkOperator : ISinkOperator { private readonly string _bootstrapServers; private readonly string _topic; diff --git a/src/Cortex.Streams.Kafka/KafkaSourceOperator.cs b/src/Cortex.Streams.Kafka/KafkaSourceOperator.cs index 4553e4e..7e668f1 100644 --- a/src/Cortex.Streams.Kafka/KafkaSourceOperator.cs +++ b/src/Cortex.Streams.Kafka/KafkaSourceOperator.cs @@ -7,7 +7,7 @@ namespace Cortex.Streams.Kafka { - public class KafkaSourceOperator : ISourceOperator + public sealed class KafkaSourceOperator : ISourceOperator { private readonly string _bootstrapServers; private readonly string _topic; @@ -15,7 +15,10 @@ public class KafkaSourceOperator : ISourceOperator private CancellationTokenSource _cts; private Task _consumeTask; - public KafkaSourceOperator(string bootstrapServers, string topic, ConsumerConfig config = null, IDeserializer deserializer = null) + public KafkaSourceOperator(string bootstrapServers, + string topic, + ConsumerConfig config = null, + IDeserializer deserializer = null) { _bootstrapServers = bootstrapServers; _topic = topic; @@ -53,7 +56,7 @@ public void Start(Action emit) } catch (OperationCanceledException) { - // Consume loop canceled + // shutting down - consume loop canceled } finally { @@ -64,8 +67,21 @@ public void Start(Action emit) public void Stop() { + if (_cts == null) + return; + _cts.Cancel(); - _consumeTask.Wait(); + try + { + _consumeTask?.Wait(); + } + catch + { + /* swallow aggregate canceled */ + } + + _consumer.Dispose(); + _cts.Dispose(); } } } From 1c46b14c3873f90766458d261c2ae2d38eb34883 Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Mon, 28 Jul 2025 15:21:01 +0200 Subject: [PATCH 3/8] v2/feature/[Add support for Keys in Pulsar Stream] #128: Enhance Cortex.Streams.Pulsar documentation and functionality Updated README.md with an introduction, features, installation instructions, and usage examples for the Pulsar Sink and Source Operators. Added sections for contributing and sponsorship. Modified `Cortex.Streams.Pulsar.csproj` to ensure README.md is always included in the output directory. Refactored `PulsarSinkOperator.cs` to support an optional key selector for key-based message sending. Changed `PulsarSourceOperator.cs` output type to `KeyValuePair` for key-aware message consumption, updating the `Start` method accordingly. --- README.md | 1 + .../Cortex.Streams.Pulsar.csproj | 17 ++- .../PulsarSinkOperator.cs | 18 ++- .../PulsarSourceOperator.cs | 36 +++-- src/Cortex.Streams.Pulsar/README.md | 127 ++++++++++++++++++ 5 files changed, 172 insertions(+), 27 deletions(-) create mode 100644 src/Cortex.Streams.Pulsar/README.md diff --git a/README.md b/README.md index 76df3ba..52bb137 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ ## Use Cases + - Real-time analytics and monitoring - Event-driven architectures - Stateful stream processing (e.g., aggregations, joins) diff --git a/src/Cortex.Streams.Pulsar/Cortex.Streams.Pulsar.csproj b/src/Cortex.Streams.Pulsar/Cortex.Streams.Pulsar.csproj index 52ddf04..639f595 100644 --- a/src/Cortex.Streams.Pulsar/Cortex.Streams.Pulsar.csproj +++ b/src/Cortex.Streams.Pulsar/Cortex.Streams.Pulsar.csproj @@ -32,11 +32,20 @@ README.md + + + + + + + + True + \ + Always + + + - - True - \ - True diff --git a/src/Cortex.Streams.Pulsar/PulsarSinkOperator.cs b/src/Cortex.Streams.Pulsar/PulsarSinkOperator.cs index a33bf9e..c8edf5a 100644 --- a/src/Cortex.Streams.Pulsar/PulsarSinkOperator.cs +++ b/src/Cortex.Streams.Pulsar/PulsarSinkOperator.cs @@ -16,7 +16,12 @@ public class PulsarSinkOperator : ISinkOperator private readonly IPulsarClient _client; private IProducer> _producer; - public PulsarSinkOperator(string serviceUrl, string topic, ISerializer serializer = null) + private readonly Func _keySelector; // optional + + public PulsarSinkOperator(string serviceUrl, + string topic, + Func keySelector = null, + ISerializer serializer = null) { _serviceUrl = serviceUrl; _topic = topic; @@ -24,6 +29,7 @@ public PulsarSinkOperator(string serviceUrl, string topic, ISerializer s _serializer ??= new DefaultJsonSerializer(); + _keySelector = keySelector; _client = PulsarClient.Builder() .ServiceUrl(new Uri(_serviceUrl)) @@ -44,7 +50,15 @@ public void Start() public void Process(TInput input) { var data = _serializer.Serialize(input); - _producer.Send(data); + if (_keySelector is null) + { + _producer.Send(data); // current behavior :contentReference[oaicite:17]{index=17} + } + else + { + var metadata = new MessageMetadata { Key = _keySelector(input) }; + _producer.Send(metadata, new ReadOnlySequence(data)); + } } public void Stop() diff --git a/src/Cortex.Streams.Pulsar/PulsarSourceOperator.cs b/src/Cortex.Streams.Pulsar/PulsarSourceOperator.cs index 30873b4..3a62955 100644 --- a/src/Cortex.Streams.Pulsar/PulsarSourceOperator.cs +++ b/src/Cortex.Streams.Pulsar/PulsarSourceOperator.cs @@ -1,16 +1,17 @@ -using DotPulsar.Abstractions; +using Cortex.Streams.Operators; +using Cortex.Streams.Pulsar.Deserializers; using DotPulsar; +using DotPulsar.Abstractions; using DotPulsar.Extensions; -using Cortex.Streams.Pulsar.Deserializers; +using System; using System.Buffers; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using System; -using Cortex.Streams.Operators; namespace Cortex.Streams.Pulsar { - public class PulsarSourceOperator : ISourceOperator + public class PulsarSourceOperator : ISourceOperator> { private readonly string _serviceUrl; private readonly ConsumerOptions> _consumerOptions; @@ -51,24 +52,17 @@ public PulsarSourceOperator(string serviceUrl, ConsumerOptions emit) + public void Start(Action> emit) { _cts = new CancellationTokenSource(); - if (_consumerOptions == null) - { - _consumer = _client.NewConsumer() + _consumer = _consumerOptions == null + ? _client.NewConsumer() .Topic(_topic) .InitialPosition(SubscriptionInitialPosition.Earliest) .SubscriptionName($"subscription-{Guid.NewGuid()}") - .Create(); - } - else - { - _consumer = _client - .CreateConsumer(_consumerOptions); - } - + .Create() + : _client.CreateConsumer(_consumerOptions); _consumeTask = Task.Run(async () => { @@ -76,15 +70,15 @@ public void Start(Action emit) { await foreach (var message in _consumer.Messages(_cts.Token)) { - var data = message.Data; - var output = _deserializer.Deserialize(data); - emit(output); + var key = message.Key ?? string.Empty; // Handle null keys gracefully + var output = _deserializer.Deserialize(message.Data); + emit(new KeyValuePair(key, output)); await _consumer.Acknowledge(message, _cts.Token); } } catch (OperationCanceledException) { - // Consume loop canceled + // Cancellation requested } finally { diff --git a/src/Cortex.Streams.Pulsar/README.md b/src/Cortex.Streams.Pulsar/README.md new file mode 100644 index 0000000..3b611fb --- /dev/null +++ b/src/Cortex.Streams.Pulsar/README.md @@ -0,0 +1,127 @@ +# Cortex.Streams.Pulsar 🧠 + +**Cortex.Streams.Pulsar** is a streaming connector for [Apache Pulsar](https://pulsar.apache.org/), designed to work seamlessly within the **Cortex Data Framework**. It enables real-time data ingestion and publication from/to Pulsar topics, now with full support for **message keys** alongside values. + +--- + +## 🌟 Features + +- 🔄 **Pulsar Source Operator**: Consume messages (key + value) from Pulsar topics. +- 🚀 **Pulsar Sink Operator**: Publish messages to Pulsar topics with optional keys. +- 🧩 **Key Support**: Allows key-based partitioning and stream grouping. +- 📦 **Seamless DSL Integration**: Easily compose with other Cortex stream operations. +- ⚡ **Built for Scale**: Backed by Pulsar’s distributed, high-throughput architecture. + +--- + +[![GitHub License](https://img.shields.io/github/license/buildersoftio/cortex)](https://github.com/buildersoftio/cortex/blob/master/LICENSE) +[![NuGet Version](https://img.shields.io/nuget/v/Cortex.Streams.Pulsar?label=Cortex.Streams.Pulsar)](https://www.nuget.org/packages/Cortex.Streams.Pulsar) +[![GitHub contributors](https://img.shields.io/github/contributors/buildersoftio/cortex)](https://github.com/buildersoftio/cortex) +[![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) + + +## 🚀 Getting Started + +### Install via NuGet + +```bash +dotnet add package Cortex.Streams.Pulsar +``` + +## ✅ Pulsar Sink Operator +In `Program.cs` or `Startup.cs`: +```csharp +using Cortex.Streams; +using Cortex.Streams.Pulsar; + +var pulsarSink = new PulsarSinkOperator("pulsar://localhost:6650", "persistent://public/default/input-topic"); + +var stream = StreamBuilder + .CreateNewStream("PulsarIngester") + .Stream() + .Sink(pulsarSink) + .Build(); + +stream.Start(); + +stream.Emit("data1"); +stream.Emit("data2"); +stream.Emit("data3"); +``` + +## ✅ Pulsar Source Operator + +```csharp +using Cortex.Streams; +using Cortex.Streams.Pulsar; + +var pulsarSource = new PulsarSourceOperator("pulsar://localhost:6650", "persistent://public/default/input-topic"); + +var stream = StreamBuilder + .CreateNewStream("PulsarProcessor") + .Stream(pulsarSource) + .Map(message => message.ToUpper()) + .Sink(processed => Console.WriteLine($"Processed: {processed}")) + .Build(); + +stream.Start(); +``` + +## 🔐 Key Use Cases + +- Partition-aware processing using message keys +- Sessionization and user-based aggregations +- Scalable event ingestion pipelines + +🧱 Prerequisites +- .NET 6.0 SDK or later +- Apache Pulsar running locally or remotely +- Add Cortex.Streams base package + + +## 💬 Contributing +We welcome contributions from the community! Whether it's reporting bugs, suggesting features, or submitting pull requests, your involvement helps improve Cortex for everyone. + +### 💬 How to Contribute +1. **Fork the Repository** +2. **Create a Feature Branch** +```bash +git checkout -b feature/YourFeature +``` +3. **Commit Your Changes** +```bash +git commit -m "Add your feature" +``` +4. **Push to Your Fork** +```bash +git push origin feature/YourFeature +``` +5. **Open a Pull Request** + +Describe your changes and submit the pull request for review. + +## 📄 License +This project is licensed under the MIT License. + +## 📚 Sponsorship +Cortex is an open-source project maintained by BuilderSoft. Your support helps us continue developing and improving Cortex. Consider sponsoring us to contribute to the future of resilient streaming platforms. + +### How to Sponsor +* **Financial Contributions**: Support us through [GitHub Sponsors](https://github.com/sponsors/buildersoftio) or other preferred platforms. +* **Corporate Sponsorship**: If your organization is interested in sponsoring Cortex, please contact us directly. + +Contact Us: cortex@buildersoft.io + + +## Contact +We'd love to hear from you! Whether you have questions, feedback, or need support, feel free to reach out. + +- Email: cortex@buildersoft.io +- Website: https://buildersoft.io +- GitHub Issues: [Cortex Data Framework Issues](https://github.com/buildersoftio/cortex/issues) +- Join our Discord Community: [![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) + + +Thank you for using Cortex Data Framework! We hope it empowers you to build scalable and efficient data processing pipelines effortlessly. + +Built with ❤️ by the Buildersoft team. From 5b08911c6664947c5b32d32800c99f9f262a1477 Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Mon, 28 Jul 2025 15:24:14 +0200 Subject: [PATCH 4/8] v2/feature/128: Update package versions for dependencies - Updated `DotPulsar` from `4.2.2` to `4.3.1` - Updated `Google.Protobuf` from `3.30.2` to `3.31.1` - Updated `protobuf-net` from `3.2.46` to `3.2.55` --- src/Cortex.Streams.Pulsar/Cortex.Streams.Pulsar.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cortex.Streams.Pulsar/Cortex.Streams.Pulsar.csproj b/src/Cortex.Streams.Pulsar/Cortex.Streams.Pulsar.csproj index 639f595..5228381 100644 --- a/src/Cortex.Streams.Pulsar/Cortex.Streams.Pulsar.csproj +++ b/src/Cortex.Streams.Pulsar/Cortex.Streams.Pulsar.csproj @@ -62,10 +62,10 @@ - + - - + + From 5db2e8db391c2d12aa7c151abbaf912777eccb25 Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Wed, 30 Jul 2025 11:50:42 +0200 Subject: [PATCH 5/8] v2/feature/[Creation of Cortex.Vectors] #107 Add Cortex.Vectors project with vector implementations This commit introduces the `Cortex.Vectors` project to the solution, featuring: - `BitVector` for fixed-length bit-packed vectors. - `DenseVector` for contiguous dense mathematical vectors. - `SparseVector` for dictionary-backed sparse vectors. - An interface `IVector` defining core vector operations. - Extension methods for cosine similarity and vector conversions. - Updated project file to target .NET 8.0 and 9.0. - Enhanced documentation in `README.md` with usage examples. - Added license file and project image. --- Cortex.sln | 6 + src/Cortex.Vectors/Abstractions/IVector.cs | 26 ++++ src/Cortex.Vectors/Assets/andyX.png | Bin 0 -> 63537 bytes src/Cortex.Vectors/Assets/license.md | 20 +++ src/Cortex.Vectors/BitVector.cs | 124 +++++++++++++++++ src/Cortex.Vectors/Cortex.Vectors.csproj | 59 ++++++++ src/Cortex.Vectors/DenseVector.cs | 151 +++++++++++++++++++++ src/Cortex.Vectors/README.md | 123 +++++++++++++++++ src/Cortex.Vectors/SparseVector.cs | 145 ++++++++++++++++++++ src/Cortex.Vectors/VectorExtensions.cs | 21 +++ 10 files changed, 675 insertions(+) create mode 100644 src/Cortex.Vectors/Abstractions/IVector.cs create mode 100644 src/Cortex.Vectors/Assets/andyX.png create mode 100644 src/Cortex.Vectors/Assets/license.md create mode 100644 src/Cortex.Vectors/BitVector.cs create mode 100644 src/Cortex.Vectors/Cortex.Vectors.csproj create mode 100644 src/Cortex.Vectors/DenseVector.cs create mode 100644 src/Cortex.Vectors/README.md create mode 100644 src/Cortex.Vectors/SparseVector.cs create mode 100644 src/Cortex.Vectors/VectorExtensions.cs diff --git a/Cortex.sln b/Cortex.sln index 025b0b9..adaf7b7 100644 --- a/Cortex.sln +++ b/Cortex.sln @@ -57,6 +57,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Streams.Elasticsearc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Types", "src\Cortex.Types\Cortex.Types.csproj", "{64E12D4C-FBB2-4004-8316-C886CBFC614B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cortex.Vectors", "src\Cortex.Vectors\Cortex.Vectors.csproj", "{268BA5C7-C6FB-4A6B-875A-492659ED4573}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -170,6 +172,10 @@ Global {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Debug|Any CPU.Build.0 = Debug|Any CPU {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Release|Any CPU.ActiveCfg = Release|Any CPU {64E12D4C-FBB2-4004-8316-C886CBFC614B}.Release|Any CPU.Build.0 = Release|Any CPU + {268BA5C7-C6FB-4A6B-875A-492659ED4573}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {268BA5C7-C6FB-4A6B-875A-492659ED4573}.Debug|Any CPU.Build.0 = Debug|Any CPU + {268BA5C7-C6FB-4A6B-875A-492659ED4573}.Release|Any CPU.ActiveCfg = Release|Any CPU + {268BA5C7-C6FB-4A6B-875A-492659ED4573}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Cortex.Vectors/Abstractions/IVector.cs b/src/Cortex.Vectors/Abstractions/IVector.cs new file mode 100644 index 0000000..f0a3b44 --- /dev/null +++ b/src/Cortex.Vectors/Abstractions/IVector.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Numerics; + +namespace Cortex.Vectors +{ + /// + /// Common contract for vector collections. + /// Provides dimension metadata and core linear‑algebra operations. + /// + /// Any IEEE‑754 floating‑point numeric type (float, double, Half, decimal). + public interface IVector : IReadOnlyList where T : IFloatingPointIeee754 + { + /// Gets the number of components in the vector. + int Dimension { get; } + + /// Dot (inner) product with another vector. + /// Thrown when vector dimensions differ. + T Dot(IVector other); + + /// Euclidean norm (L2). + T Norm(); + + /// Returns a unit‑length copy of this vector. + IVector Normalize(); + } +} diff --git a/src/Cortex.Vectors/Assets/andyX.png b/src/Cortex.Vectors/Assets/andyX.png new file mode 100644 index 0000000000000000000000000000000000000000..101a1fb10887915ba6cd81f7493120090cfab590 GIT binary patch literal 63537 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 V*Ppl8Hi;(v?^jgV{@+>5{|{p6e{lc+ literal 0 HcmV?d00001 diff --git a/src/Cortex.Vectors/Assets/license.md b/src/Cortex.Vectors/Assets/license.md new file mode 100644 index 0000000..3c845d4 --- /dev/null +++ b/src/Cortex.Vectors/Assets/license.md @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2025 Buildersoft + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Cortex.Vectors/BitVector.cs b/src/Cortex.Vectors/BitVector.cs new file mode 100644 index 0000000..69c9c99 --- /dev/null +++ b/src/Cortex.Vectors/BitVector.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Cortex.Vectors +{ + /// + /// Fixed‑length bit‑packed vector that implements for any IEEE‑754 type . + /// Each bit encodes 0 → , 1 → . + /// + public sealed class BitVector : IVector, IEquatable> where T : IFloatingPointIeee754 + { + private readonly ulong[] _blocks; + + public int Dimension { get; } + public int Count => Dimension; + + #region Construction + public BitVector(int dimension) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dimension); + Dimension = dimension; + _blocks = new ulong[(dimension + 63) >> 6]; + } + + /// Create from indices that should be set to 1. + public BitVector(int dimension, IEnumerable oneIndices) : this(dimension) + { + foreach (var idx in oneIndices) SetBit(idx, true); + } + + /// Create from a span of bools. + public BitVector(ReadOnlySpan bits) : this(bits.Length) + { + for (int i = 0; i < bits.Length; i++) if (bits[i]) SetBit(i, true); + } + #endregion + + #region Bit helpers + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static (int blk, int off) Loc(int idx) => (idx >> 6, idx & 63); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool GetBit(int idx) + { + if ((uint)idx >= Dimension) throw new IndexOutOfRangeException(); + var (b, o) = Loc(idx); + return (_blocks[b] & (1UL << o)) != 0UL; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void SetBit(int idx, bool value) + { + if ((uint)idx >= Dimension) throw new IndexOutOfRangeException(); + var (b, o) = Loc(idx); + if (value) _blocks[b] |= 1UL << o; else _blocks[b] &= ~(1UL << o); + } + public int PopCount() + { + int c = 0; foreach (var v in _blocks) c += BitOperations.PopCount(v); return c; + } + #endregion + + #region IVector Implementation + public T this[int index] + { + get => GetBit(index) ? T.One : T.Zero; + } + + public T Dot(IVector other) + { + if (other is BitVector bv) + { + ValidateSameDimension(bv); + int count = 0; + for (int i = 0; i < _blocks.Length; i++) count += BitOperations.PopCount(_blocks[i] & bv._blocks[i]); + return T.CreateTruncating(count); + } + else + { + ValidateSameDimension(other); + T sum = T.Zero; + for (int i = 0; i < Dimension; i++) sum += this[i] * other[i]; + return sum; + } + } + + public T Norm() => T.Sqrt(T.CreateTruncating(PopCount())); + + public IVector Normalize() + { + var n = Norm(); + if (n == T.Zero) throw new InvalidOperationException("Cannot normalize zero bit‑vector."); + var inv = T.One / n; + var data = new T[Dimension]; + for (int i = 0; i < Dimension; i++) if (GetBit(i)) data[i] = inv; // zeros already default + return new DenseVector(data); + } + #endregion + + #region IEnumerable + public IEnumerator GetEnumerator() + { + for (int i = 0; i < Dimension; i++) yield return this[i]; + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + #endregion + + #region Equality & HashCode + public bool Equals(BitVector? other) + { + if (other is null || other.Dimension != Dimension) return false; + for (int i = 0; i < _blocks.Length; i++) if (_blocks[i] != other._blocks[i]) return false; + return true; + } + public override bool Equals(object? obj) => obj is BitVector bv && Equals(bv); + public override int GetHashCode() => HashCode.Combine(Dimension, _blocks.Length > 0 ? _blocks[0] : 0UL); + #endregion + + private void ValidateSameDimension(IVector other) + { + if (other.Dimension != Dimension) throw new ArgumentException("Vector dimensions must match.", nameof(other)); + } + } +} diff --git a/src/Cortex.Vectors/Cortex.Vectors.csproj b/src/Cortex.Vectors/Cortex.Vectors.csproj new file mode 100644 index 0000000..8e1bda8 --- /dev/null +++ b/src/Cortex.Vectors/Cortex.Vectors.csproj @@ -0,0 +1,59 @@ + + + + net9.0;net8.0 + + 2.0.0 + 2.0.0 + Buildersoft Cortex Framework + Buildersoft + Buildersoft,EnesHoxha + Copyright © Buildersoft 2025 + + Cortex Data Framework is a robust, extensible platform designed to facilitate real-time data streaming, processing, and state management. It provides developers with a comprehensive suite of tools and libraries to build scalable, high-performance data pipelines tailored to diverse use cases. By abstracting underlying streaming technologies and state management solutions, Cortex Data Framework enables seamless integration, simplified development workflows, and enhanced maintainability for complex data-driven applications. + + + https://github.com/buildersoftio/cortex + cortex;machine‑learning;vector;ai;streaming + + 2.0.0 + license.md + andyX.png + Cortex.Vectors + True + True + True + git + Just as the Cortex in our brains handles complex processing efficiently, Cortex Data Framework brings brainpower to your data management! + https://buildersoft.io/ + README.md + + + + + + + + + + True + \ + Always + + + + + True + + + + True + + + + + + + + + diff --git a/src/Cortex.Vectors/DenseVector.cs b/src/Cortex.Vectors/DenseVector.cs new file mode 100644 index 0000000..15c71e8 --- /dev/null +++ b/src/Cortex.Vectors/DenseVector.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Cortex.Vectors +{ + /// + /// Contiguous dense representation of a mathematical vector. + /// Suitable for small‑to‑medium dimensions (< 10⁶) that are mostly non‑zero. + /// + /// Floating‑point element type. + public sealed class DenseVector : IVector, IEquatable> where T : IFloatingPointIeee754 + { + private readonly T[] _data; + + #region Construction + + public DenseVector(ReadOnlySpan span) + { + _data = span.ToArray(); + } + + public DenseVector(params T[] values) + { + _data = values.Length == 0 + ? throw new ArgumentException("Vector must have at least one component.", nameof(values)) + : (T[])values.Clone(); + } + + /// Creates a zero‑filled vector of given dimension. + public static DenseVector Zeros(int dimension) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dimension); + return new DenseVector(new T[dimension]); + } + + /// Creates a vector where every component equals . + public static DenseVector Filled(int dimension, T value) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dimension); + var data = new T[dimension]; + Array.Fill(data, value); + return new DenseVector(data); + } + + #endregion + + #region IVector & IReadOnlyList Implementation + + public int Dimension => _data.Length; + + public int Count => _data.Length; // IReadOnlyCollection implementation + + public T this[int index] => _data[index]; + + public T Dot(IVector other) + { + ValidateSameDimension(other); + T sum = T.Zero; + for (int i = 0; i < _data.Length; i++) + sum += _data[i] * other[i]; + return sum; + } + + public T Norm() + { + T sumSq = T.Zero; + foreach (var v in _data) + sumSq += v * v; + return T.Sqrt(sumSq); + } + + public IVector Normalize() + { + var n = Norm(); + if (n == T.Zero) + throw new InvalidOperationException("Cannot normalize the zero vector."); + var scaled = new T[_data.Length]; + for (int i = 0; i < _data.Length; i++) + scaled[i] = _data[i] / n; + return new DenseVector(scaled); + } + + #endregion + + #region Arithmetic Operators + + public static DenseVector operator +(DenseVector left, DenseVector right) + { + left.ValidateSameDimension(right); + var result = new T[left.Dimension]; + for (int i = 0; i < result.Length; i++) + result[i] = left._data[i] + right._data[i]; + return new DenseVector(result); + } + + public static DenseVector operator -(DenseVector left, DenseVector right) + { + left.ValidateSameDimension(right); + var result = new T[left.Dimension]; + for (int i = 0; i < result.Length; i++) + result[i] = left._data[i] - right._data[i]; + return new DenseVector(result); + } + + public static DenseVector operator *(DenseVector vector, T scalar) + { + var result = new T[vector.Dimension]; + for (int i = 0; i < result.Length; i++) + result[i] = vector._data[i] * scalar; + return new DenseVector(result); + } + + public static DenseVector operator *(T scalar, DenseVector vector) => vector * scalar; + + #endregion + + #region Equality & Hash + + public bool Equals(DenseVector? other) + { + if (other is null || other.Dimension != Dimension) return false; + for (int i = 0; i < Dimension; i++) + if (_data[i] != other._data[i]) return false; + return true; + } + + public override bool Equals(object? obj) => obj is DenseVector v && Equals(v); + + public override int GetHashCode() => HashCode.Combine(Dimension, _data[0], _data[^1]); + + #endregion + + #region IEnumerable Implementation + + public IEnumerator GetEnumerator() => ((IEnumerable)_data).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _data.GetEnumerator(); + + #endregion + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidateSameDimension(IVector other) + { + if (other.Dimension != Dimension) + throw new ArgumentException($"Vector dimensions must match (this: {Dimension}, other: {other.Dimension}).", nameof(other)); + } + } + +} diff --git a/src/Cortex.Vectors/README.md b/src/Cortex.Vectors/README.md new file mode 100644 index 0000000..773ac55 --- /dev/null +++ b/src/Cortex.Vectors/README.md @@ -0,0 +1,123 @@ +# Cortex.Vectors 🧠 + +**Cortex.Vectors** is a High‑performance vector types—Dense, Sparse, and Bit—for AI & for .NET. + + +Built as part of the [Cortex Data Framework](https://github.com/buildersoftio/cortex), this library offers High‑performance vector types—Dense, Sparse, and Bit—for AI for: + + +- ✨ Generic‑math powered (IFloatingPointIeee754): works with float, double, decimal, … +- 🟢 DenseVector – contiguous storage, SIMD‑friendly operations +- 🔵 SparseVector – dictionary‑backed, memory‑efficient for huge, mostly‑zero spaces +- 🟡 BitVector – bit‑packed booleans with popcount & logical ops +- ⚙️ Core ops out‑of‑the‑box: dot product, L2 norm, cosine similarity, scaling, +/‑ + +--- + +[![GitHub License](https://img.shields.io/github/license/buildersoftio/cortex)](https://github.com/buildersoftio/cortex/blob/master/LICENSE) +[![NuGet Version](https://img.shields.io/nuget/v/Cortex.Vectors?label=Cortex.Vectors)](https://www.nuget.org/packages/Cortex.Vectors) +[![GitHub contributors](https://img.shields.io/github/contributors/buildersoftio/cortex)](https://github.com/buildersoftio/cortex) +[![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) + + +## 🚀 Getting Started + +### Install via NuGet + +```bash +dotnet add package Cortex.Vectors +``` + +## DenseVector +```csharp +using Cortex.Vectors; + +// (1, 2, 3) +var a = new DenseVector(1f, 2f, 3f); + +// (0.5, 0.5, 0.5) +var b = DenseVector.Filled(3, 0.5f); + +float dot = a.Dot(b); // = 3.0 +var normA = a.Norm(); // ≈ 3.7417 +var unitA = a.Normalize(); // unit length +float cosine = a.CosineSimilarity(b); +``` + +## SparseVector +```csharp +using Cortex.Vectors; +using System.Collections.Generic; + +// 1‑million‑dimensional vector with two non‑zeros +var sv = new SparseVector( + dimension: 1_000_000, + nonZero: new[] + { + new KeyValuePair(42, 1.0), + new KeyValuePair(123456, 2.5) + }); + +double l2 = sv.Norm(); // √(1² + 2.5²) +var unit = sv.Normalize(); +``` + +## BitVector + +```csharp +using Cortex.Vectors; + +// length 128, bits 0, 3, and 5 set to 1 +var bv = new BitVector(128, new[] { 0, 3, 5 }); + +int ones = bv.PopCount(); // 3 +float selfDot = bv.Dot(bv); // 3.0 (generic type ⇒ float) +var l2 = bv.Norm(); // √3 +``` + +## 💬 Contributing +We welcome contributions from the community! Whether it's reporting bugs, suggesting features, or submitting pull requests, your involvement helps improve Cortex for everyone. + +### 💬 How to Contribute +1. **Fork the Repository** +2. **Create a Feature Branch** +```bash +git checkout -b feature/YourFeature +``` +3. **Commit Your Changes** +```bash +git commit -m "Add your feature" +``` +4. **Push to Your Fork** +```bash +git push origin feature/YourFeature +``` +5. **Open a Pull Request** + +Describe your changes and submit the pull request for review. + +## 📄 License +This project is licensed under the MIT License. + +## 📚 Sponsorship +Cortex is an open-source project maintained by BuilderSoft. Your support helps us continue developing and improving Cortex. Consider sponsoring us to contribute to the future of resilient streaming platforms. + +### How to Sponsor +* **Financial Contributions**: Support us through [GitHub Sponsors](https://github.com/sponsors/buildersoftio) or other preferred platforms. +* **Corporate Sponsorship**: If your organization is interested in sponsoring Cortex, please contact us directly. + +Contact Us: cortex@buildersoft.io + + +## Contact +We'd love to hear from you! Whether you have questions, feedback, or need support, feel free to reach out. + +- Email: cortex@buildersoft.io +- Website: https://buildersoft.io +- GitHub Issues: [Cortex Data Framework Issues](https://github.com/buildersoftio/cortex/issues) +- Join our Discord Community: [![Discord Shield](https://discord.com/api/guilds/1310034212371566612/widget.png?style=shield)](https://discord.gg/JnMJV33QHu) + + +Thank you for using Cortex Data Framework! We hope it empowers you to build scalable and efficient data processing pipelines effortlessly. + +Built with ❤️ by the Buildersoft team. diff --git a/src/Cortex.Vectors/SparseVector.cs b/src/Cortex.Vectors/SparseVector.cs new file mode 100644 index 0000000..bea7c51 --- /dev/null +++ b/src/Cortex.Vectors/SparseVector.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Cortex.Vectors +{ + public sealed class SparseVector : IVector, IEquatable> where T : IFloatingPointIeee754 + { + private readonly int _dimension; + private readonly Dictionary _values; + + #region Construction + public SparseVector(int dimension) : this(dimension, Enumerable.Empty>()) { } + + public SparseVector(int dimension, IEnumerable> nonZero) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dimension); + _dimension = dimension; + _values = new Dictionary(); + foreach (var (i, val) in nonZero) + { + if (i < 0 || i >= dimension) throw new ArgumentOutOfRangeException(nameof(nonZero), "Index out of range."); + if (val != T.Zero) _values[i] = val; + } + } + + /// Creates a sparse vector where the provided indices hold the same . + public static SparseVector FromIndices(int dimension, IEnumerable indices, T value) + => new SparseVector(dimension, indices.Select(i => new KeyValuePair(i, value))); + #endregion + + #region IReadOnlyList Implementation + public int Dimension => _dimension; + public int Count => _dimension; // total logical length + public int NonZeroCount => _values.Count; + + public T this[int index] + { + get + { + if ((uint)index >= _dimension) throw new IndexOutOfRangeException(); + return _values.TryGetValue(index, out var v) ? v : T.Zero; + } + } + #endregion + + #region Core Vector Operations + public T Dot(IVector other) + { + ValidateSameDimension(other); + T sum = T.Zero; + foreach (var (i, v) in _values) sum += v * other[i]; + return sum; + } + + public T Norm() + { + T sumSq = T.Zero; + foreach (var v in _values.Values) sumSq += v * v; + return T.Sqrt(sumSq); + } + + public IVector Normalize() + { + var n = Norm(); + if (n == T.Zero) throw new InvalidOperationException("Cannot normalize zero vector."); + var scaled = _values.Select(kv => new KeyValuePair(kv.Key, kv.Value / n)); + return new SparseVector(_dimension, scaled); + } + #endregion + + #region Operators + public static SparseVector operator +(SparseVector a, SparseVector b) + { + a.ValidateSameDimension(b); + var result = new Dictionary(a._values); + foreach (var (i, v) in b._values) + { + if (result.TryGetValue(i, out var existing)) + { + var sum = existing + v; + if (sum == T.Zero) result.Remove(i); else result[i] = sum; + } + else result[i] = v; + } + return new SparseVector(a._dimension, result); + } + + public static SparseVector operator -(SparseVector a, SparseVector b) + { + a.ValidateSameDimension(b); + var result = new Dictionary(a._values); + foreach (var (i, v) in b._values) + { + if (result.TryGetValue(i, out var existing)) + { + var diff = existing - v; + if (diff == T.Zero) result.Remove(i); else result[i] = diff; + } + else if (v != T.Zero) result[i] = -v; + } + return new SparseVector(a._dimension, result); + } + + public static SparseVector operator *(SparseVector vector, T scalar) + { + if (scalar == T.Zero) return new SparseVector(vector._dimension); + var result = vector._values.ToDictionary(k => k.Key, k => k.Value * scalar); + return new SparseVector(vector._dimension, result); + } + + public static SparseVector operator *(T scalar, SparseVector vector) => vector * scalar; + #endregion + + #region Equality & Hashing + public bool Equals(SparseVector? other) + { + if (other is null || other._dimension != _dimension || other._values.Count != _values.Count) return false; + foreach (var kv in _values) + if (!other._values.TryGetValue(kv.Key, out var v) || v != kv.Value) return false; + return true; + } + + public override bool Equals(object? obj) => obj is SparseVector sv && Equals(sv); + public override int GetHashCode() => HashCode.Combine(_dimension, _values.Count); + #endregion + + #region Enumeration + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _dimension; i++) yield return this[i]; + } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + #endregion + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ValidateSameDimension(IVector other) + { + if (other.Dimension != _dimension) throw new ArgumentException("Vector dimensions must match.", nameof(other)); + } + } +} diff --git a/src/Cortex.Vectors/VectorExtensions.cs b/src/Cortex.Vectors/VectorExtensions.cs new file mode 100644 index 0000000..6ee463e --- /dev/null +++ b/src/Cortex.Vectors/VectorExtensions.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +namespace Cortex.Vectors +{ + public static class VectorExtensions + { + public static T CosineSimilarity(this IVector a, IVector b) where T : IFloatingPointIeee754 + { + var denom = a.Norm() * b.Norm(); + return denom == T.Zero ? T.Zero : a.Dot(b) / denom; + } + + public static SparseVector ToSparse(this DenseVector v) where T : IFloatingPointIeee754 + => new SparseVector(v.Dimension, + Enumerable.Range(0, v.Dimension) + .Where(i => v[i] != T.Zero) + .Select(i => new KeyValuePair(i, v[i]))); + } +} From 4a98cd3aab341ff2df7265fe764c2a050b7f0f9c Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Wed, 30 Jul 2025 11:58:23 +0200 Subject: [PATCH 6/8] Add Cortex.Vectors section to README.md Introduced a new section for **Cortex.Vectors**, highlighting its high-performance vector types for AI, including Dense, Sparse, and Bit types. Added a NuGet version badge for easy access to the package. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 4b2cc65..b730ba5 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,9 @@ - **Cortex.Mediator.Behaviors.FluentValidation:** implementation of the FluentValidation validation for Commands and Queries [![NuGet Version](https://img.shields.io/nuget/v/Cortex.Mediator.Behaviors.FluentValidation?label=Cortex.Mediator.Behaviors.FluentValidation)](https://www.nuget.org/packages/Cortex.Mediator.Behaviors.FluentValidation) +- **Cortex.Vectors:** is a High‑performance vector types—Dense, Sparse, and Bit—for AI. +[![NuGet Version](https://img.shields.io/nuget/v/Cortex.Vectors?label=Cortex.Vectors)](https://www.nuget.org/packages/Cortex.Vectors) + ## Getting Started From b0b241084014b658caf556ecc3b0955afcf033ee Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Wed, 8 Oct 2025 15:44:15 +0200 Subject: [PATCH 7/8] v2/feature/141 : Cortex.Mediator - Add Non Returning Command Interface (ICommand) Add support for non-returning CQRS commands Introduced a non-generic `ICommand` interface for commands that do not return values, alongside a corresponding `ICommandHandler` interface. Added `ICommandPipelineBehavior` and `CommandHandlerDelegate` to enable pipeline behaviors for non-returning commands. Updated `MediatorOptions` to manage both returning and non-returning command behaviors, including the addition of `VoidCommandBehaviors`. Enhanced `MediatorOptionsExtensions` and `ServiceCollectionExtensions` to register default and custom behaviors for non-returning commands. Extended `IMediator` with a `SendCommandAsync` method for non-returning commands. Updated the `Mediator` implementation to handle non-returning commands and added a `PipelineBehaviorNextDelegate` for behavior chaining. Added `VoidLoggingCommandBehavior` to log execution details for non-returning commands. Refactored existing code to ensure compatibility with the new non-returning command infrastructure. --- .../Behaviors/VoidLoggingCommandBehavior.cs | 51 ++++++++++++++++ src/Cortex.Mediator/Commands/ICommand.cs | 10 +++ .../Commands/ICommandHandler.cs | 19 ++++++ .../Commands/ICommandPipelineBehavior.cs | 25 ++++++++ .../DependencyInjection/MediatorOptions.cs | 61 ++++++++++--------- .../MediatorOptionsExtensions.cs | 3 +- .../ServiceCollectionExtensions.cs | 14 +++++ src/Cortex.Mediator/IMediator.cs | 5 ++ src/Cortex.Mediator/Mediator.cs | 40 +++++++++++- 9 files changed, 195 insertions(+), 33 deletions(-) create mode 100644 src/Cortex.Mediator/Behaviors/VoidLoggingCommandBehavior.cs diff --git a/src/Cortex.Mediator/Behaviors/VoidLoggingCommandBehavior.cs b/src/Cortex.Mediator/Behaviors/VoidLoggingCommandBehavior.cs new file mode 100644 index 0000000..b6134f3 --- /dev/null +++ b/src/Cortex.Mediator/Behaviors/VoidLoggingCommandBehavior.cs @@ -0,0 +1,51 @@ +using Cortex.Mediator.Commands; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Cortex.Mediator.Behaviors +{ + + public sealed class LoggingCommandBehavior : ICommandPipelineBehavior where TCommand : ICommand + { + private readonly ILogger> _logger; + + public LoggingCommandBehavior(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle( + TCommand command, + CommandHandlerDelegate next, + CancellationToken cancellationToken) + { + var commandName = typeof(TCommand).Name; + _logger.LogInformation("Executing command {CommandName}", commandName); + + var stopwatch = Stopwatch.StartNew(); // start timing + try + { + await next(); + + stopwatch.Stop(); + _logger.LogInformation( + "Command {CommandName} executed successfully in {ElapsedMilliseconds} ms", + commandName, + stopwatch.ElapsedMilliseconds); + } + catch (Exception ex) + { + stopwatch.Stop(); + _logger.LogError( + ex, + "Error executing command {CommandName} after {ElapsedMilliseconds} ms", + commandName, + stopwatch.ElapsedMilliseconds); + throw; + } + } + } +} diff --git a/src/Cortex.Mediator/Commands/ICommand.cs b/src/Cortex.Mediator/Commands/ICommand.cs index fdc2226..47b56c9 100644 --- a/src/Cortex.Mediator/Commands/ICommand.cs +++ b/src/Cortex.Mediator/Commands/ICommand.cs @@ -8,4 +8,14 @@ public interface ICommand { } + + // feature #141 + + /// + /// Represents a command in the CQRS pattern. + /// Commands are used to change the system state and do not return a value. + /// + public interface ICommand + { + } } diff --git a/src/Cortex.Mediator/Commands/ICommandHandler.cs b/src/Cortex.Mediator/Commands/ICommandHandler.cs index 574386c..c2cf6a7 100644 --- a/src/Cortex.Mediator/Commands/ICommandHandler.cs +++ b/src/Cortex.Mediator/Commands/ICommandHandler.cs @@ -18,4 +18,23 @@ public interface ICommandHandler /// The cancellation token. Task Handle(TCommand command, CancellationToken cancellationToken); } + + + + // feature #141 + + /// + /// Defines a handler for a command. + /// + /// The type of command being handled. + public interface ICommandHandler + where TCommand : ICommand + { + /// + /// Handles the specified command. + /// + /// The command to handle. + /// The cancellation token. + Task Handle(TCommand command, CancellationToken cancellationToken); + } } diff --git a/src/Cortex.Mediator/Commands/ICommandPipelineBehavior.cs b/src/Cortex.Mediator/Commands/ICommandPipelineBehavior.cs index b1e4a9c..306e22e 100644 --- a/src/Cortex.Mediator/Commands/ICommandPipelineBehavior.cs +++ b/src/Cortex.Mediator/Commands/ICommandPipelineBehavior.cs @@ -19,8 +19,33 @@ Task Handle( CancellationToken cancellationToken); } + + // For non returning commands + // feature #141 + + /// + /// Defines a pipeline behavior for wrapping command handlers. + /// + /// The type of command being handled. + public interface ICommandPipelineBehavior + where TCommand : ICommand + { + /// + /// Handles the command and invokes the next behavior in the pipeline. + /// + Task Handle( + TCommand command, + CommandHandlerDelegate next, + CancellationToken cancellationToken); + } + /// /// Represents a delegate that wraps the command handler execution. /// public delegate Task CommandHandlerDelegate(); + + /// + /// Represents a delegate that wraps the command handler execution. + /// + public delegate Task CommandHandlerDelegate(); } diff --git a/src/Cortex.Mediator/DependencyInjection/MediatorOptions.cs b/src/Cortex.Mediator/DependencyInjection/MediatorOptions.cs index 415dfea..15ee488 100644 --- a/src/Cortex.Mediator/DependencyInjection/MediatorOptions.cs +++ b/src/Cortex.Mediator/DependencyInjection/MediatorOptions.cs @@ -9,6 +9,7 @@ namespace Cortex.Mediator.DependencyInjection public class MediatorOptions { internal List CommandBehaviors { get; } = new(); + internal List VoidCommandBehaviors { get; } = new(); internal List QueryBehaviors { get; } = new(); public bool OnlyPublicClasses { get; set; } = true; @@ -23,21 +24,25 @@ public MediatorOptions AddCommandPipelineBehavior() var behaviorType = typeof(TBehavior); if (behaviorType.IsGenericTypeDefinition) - { throw new ArgumentException("Open generic types must be registered using AddOpenCommandPipelineBehavior"); - } - var implementsInterface = behaviorType - .GetInterfaces() - .Any(i => i.IsGenericType && - i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<,>)); + var implementsReturning = + behaviorType.GetInterfaces().Any(i => i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<,>)); - if (!implementsInterface) - { - throw new ArgumentException("Type must implement ICommandPipelineBehavior<,>"); - } + var implementsNonReturning = + behaviorType.GetInterfaces().Any(i => i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<>)); + + if (!implementsReturning && !implementsNonReturning) + throw new ArgumentException("Type must implement ICommandPipelineBehavior<,> or ICommandPipelineBehavior<>"); + + if (implementsReturning) + CommandBehaviors.Add(behaviorType); + + if (implementsNonReturning) + VoidCommandBehaviors.Add(behaviorType); - CommandBehaviors.Add(behaviorType); return this; } @@ -47,29 +52,25 @@ public MediatorOptions AddCommandPipelineBehavior() public MediatorOptions AddOpenCommandPipelineBehavior(Type openGenericBehaviorType) { if (!openGenericBehaviorType.IsGenericTypeDefinition) - { throw new ArgumentException("Type must be an open generic type definition"); - } - var implementsInterface = openGenericBehaviorType - .GetInterfaces() - .Any(i => i.IsGenericType && - i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<,>)); + var implementsReturning = + openGenericBehaviorType.GetInterfaces().Any(i => i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<,>)); - // For open generics, interface might not appear in GetInterfaces() yet; check by definition instead. - if (!implementsInterface && - !(openGenericBehaviorType.IsGenericTypeDefinition && - openGenericBehaviorType.GetGenericTypeDefinition() == openGenericBehaviorType)) - { - // Fall back to checking generic arguments count to give a clear error - var ok = openGenericBehaviorType.GetGenericArguments().Length == 2; - if (!ok) - { - throw new ArgumentException("Type must implement ICommandPipelineBehavior<,>"); - } - } + var implementsNonReturning = + openGenericBehaviorType.GetInterfaces().Any(i => i.IsGenericType && + i.GetGenericTypeDefinition() == typeof(ICommandPipelineBehavior<>)); + + if (!implementsReturning && !implementsNonReturning) + throw new ArgumentException("Type must implement ICommandPipelineBehavior<,> or ICommandPipelineBehavior<>"); + + if (implementsReturning) + CommandBehaviors.Add(openGenericBehaviorType); + + if (implementsNonReturning) + VoidCommandBehaviors.Add(openGenericBehaviorType); - CommandBehaviors.Add(openGenericBehaviorType); return this; } diff --git a/src/Cortex.Mediator/DependencyInjection/MediatorOptionsExtensions.cs b/src/Cortex.Mediator/DependencyInjection/MediatorOptionsExtensions.cs index 76f8d07..0fce80c 100644 --- a/src/Cortex.Mediator/DependencyInjection/MediatorOptionsExtensions.cs +++ b/src/Cortex.Mediator/DependencyInjection/MediatorOptionsExtensions.cs @@ -9,7 +9,8 @@ public static MediatorOptions AddDefaultBehaviors(this MediatorOptions options) return options // Register the open generic logging behavior for commands that return TResult .AddOpenCommandPipelineBehavior(typeof(LoggingCommandBehavior<,>)) - .AddOpenQueryPipelineBehavior(typeof(LoggingQueryBehavior<,>)); + .AddOpenQueryPipelineBehavior(typeof(LoggingQueryBehavior<,>)) + .AddOpenCommandPipelineBehavior(typeof(LoggingCommandBehavior<>)); // Add void command logging } } } diff --git a/src/Cortex.Mediator/DependencyInjection/ServiceCollectionExtensions.cs b/src/Cortex.Mediator/DependencyInjection/ServiceCollectionExtensions.cs index fc54ea6..837fe50 100644 --- a/src/Cortex.Mediator/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Cortex.Mediator/DependencyInjection/ServiceCollectionExtensions.cs @@ -49,6 +49,14 @@ private static void RegisterHandlers( .AsImplementedInterfaces() .WithScopedLifetime()); + // feature #141 - Register void command handlers + services.Scan(scan => scan + .FromAssemblies(assemblies) + .AddClasses(classes => classes + .AssignableTo(typeof(ICommandHandler<>)), options.OnlyPublicClasses) + .AsImplementedInterfaces() + .WithScopedLifetime()); + services.Scan(scan => scan .FromAssemblies(assemblies) .AddClasses(classes => classes @@ -72,6 +80,12 @@ private static void RegisterPipelineBehaviors(IServiceCollection services, Media services.AddTransient(typeof(ICommandPipelineBehavior<,>), behaviorType); } + // feature #141 - Register non-returning command pipeline behaviors + foreach (var behaviorType in options.VoidCommandBehaviors) + { + services.AddTransient(typeof(ICommandPipelineBehavior<>), behaviorType); + } + // Query behaviors (if needed) foreach (var behaviorType in options.QueryBehaviors) { diff --git a/src/Cortex.Mediator/IMediator.cs b/src/Cortex.Mediator/IMediator.cs index 7eb104c..eb46dd0 100644 --- a/src/Cortex.Mediator/IMediator.cs +++ b/src/Cortex.Mediator/IMediator.cs @@ -16,6 +16,11 @@ Task SendCommandAsync( CancellationToken cancellationToken = default) where TCommand : ICommand; + Task SendCommandAsync( + TCommand command, + CancellationToken cancellationToken = default) + where TCommand : ICommand; + Task SendQueryAsync( TQuery query, CancellationToken cancellationToken = default) diff --git a/src/Cortex.Mediator/Mediator.cs b/src/Cortex.Mediator/Mediator.cs index cd0c521..d5087f3 100644 --- a/src/Cortex.Mediator/Mediator.cs +++ b/src/Cortex.Mediator/Mediator.cs @@ -22,7 +22,7 @@ public Mediator(IServiceProvider serviceProvider) } public async Task SendCommandAsync(TCommand command, CancellationToken cancellationToken = default) - where TCommand : ICommand + where TCommand : ICommand { var handler = _serviceProvider.GetRequiredService>(); @@ -31,7 +31,19 @@ public async Task SendCommandAsync(TCommand command, handler = new PipelineBehaviorNextDelegate(behavior, handler); } - return await handler.Handle(command, cancellationToken); + return await handler.Handle(command, cancellationToken); + } + + public async Task SendCommandAsync(TCommand command, CancellationToken cancellationToken = default) where TCommand : ICommand + { + var handler = _serviceProvider.GetRequiredService>(); + + foreach (var behavior in _serviceProvider.GetServices>().Reverse()) + { + handler = new PipelineBehaviorNextDelegate(behavior, handler); + } + + await handler.Handle(command, cancellationToken); } public async Task SendQueryAsync(TQuery query, CancellationToken cancellationToken = default) @@ -57,6 +69,7 @@ public async Task PublishAsync( await Task.WhenAll(tasks); } + private class PipelineBehaviorNextDelegate : ICommandHandler where TCommand : ICommand { @@ -80,6 +93,29 @@ public Task Handle(TCommand command, CancellationToken cancellationToke } } + private class PipelineBehaviorNextDelegate : ICommandHandler + where TCommand : ICommand + { + private readonly ICommandPipelineBehavior _behavior; + private readonly ICommandHandler _next; + + public PipelineBehaviorNextDelegate( + ICommandPipelineBehavior behavior, + ICommandHandler next) + { + _behavior = behavior; + _next = next; + } + + public Task Handle(TCommand command, CancellationToken cancellationToken) + { + return _behavior.Handle( + command, + () => _next.Handle(command, cancellationToken), + cancellationToken); + } + } + private class QueryPipelineBehaviorNextDelegate : IQueryHandler where TQuery : IQuery From 2e327a40886ebaeab2bc4ea050706f01262a5cb3 Mon Sep 17 00:00:00 2001 From: Enes Hoxha Date: Wed, 8 Oct 2025 16:09:16 +0200 Subject: [PATCH 8/8] v2/feature/ #102: Add EmitAsync Add async Emit and StreamStatuses enum to IStream Introduced `EmitAsync` in the `IStream` interface and `Stream` class to support asynchronous data emission with `CancellationToken`. Added the `StreamStatuses` enum to replace string-based stream status, providing a strongly-typed representation. Updated `GetStatus` to use the new enum. Enhanced documentation and ensured backward compatibility. --- src/Cortex.Streams/Abstractions/IStream.cs | 28 +++++++++++++++++- src/Cortex.Streams/Stream.cs | 34 ++++++++++++++++++++-- src/Cortex.Streams/StreamStatuses.cs | 8 +++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/Cortex.Streams/StreamStatuses.cs diff --git a/src/Cortex.Streams/Abstractions/IStream.cs b/src/Cortex.Streams/Abstractions/IStream.cs index 2c9e895..d382080 100644 --- a/src/Cortex.Streams/Abstractions/IStream.cs +++ b/src/Cortex.Streams/Abstractions/IStream.cs @@ -1,15 +1,41 @@ using Cortex.States; using Cortex.Streams.Operators; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace Cortex.Streams { public interface IStream { + /// + /// Start the stream processing. + /// void Start(); + + /// + /// Stops the stream processing. + /// void Stop(); + + /// + /// Processes the specified input value and emits it to the underlying stream. + /// + /// The input value to be emitted. The meaning and requirements of this value depend on the implementation. void Emit(TIn value); - string GetStatus(); + + // feature #102: Support async emit with cancellation token + + /// + /// Asynchronously emits the specified value to the underlying stream. + /// + /// The value to emit. The meaning and requirements of this value depend on the implementation. + /// A cancellation token that can be used to cancel the emit operation. + /// A task that represents the asynchronous emit operation. + Task EmitAsync(TIn value, CancellationToken cancellationToken = default); + + StreamStatuses GetStatus(); + IReadOnlyDictionary> GetBranches(); TStateStore GetStateStoreByName(string name) where TStateStore : IDataStore; diff --git a/src/Cortex.Streams/Stream.cs b/src/Cortex.Streams/Stream.cs index d062a94..acc2425 100644 --- a/src/Cortex.Streams/Stream.cs +++ b/src/Cortex.Streams/Stream.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace Cortex.Streams { @@ -87,9 +89,9 @@ public void Stop() /// Gets the current status of the stream. /// /// A string indicating whether the stream is running or stopped. - public string GetStatus() + public StreamStatuses GetStatus() { - return _isStarted ? "Running" : "Stopped"; + return _isStarted ? StreamStatuses.RUNNING : StreamStatuses.NOT_RUNNING; } /// @@ -113,6 +115,34 @@ public void Emit(TIn value) } } + // feature #102: Support async emit with cancellation token + + /// + /// Asynchronously Emits data into the stream when no source operator is used. + /// + /// The value to emit. The meaning and requirements of this value depend on the implementation. + /// A cancellation token that can be used to cancel the emit operation. + /// A task that represents the asynchronous emit operation. + public Task EmitAsync(TIn value, CancellationToken cancellationToken = default) + { + if (!_isStarted) + throw new InvalidOperationException("Stream has not been started."); + + if (_operatorChain is SourceOperatorAdapter) + throw new InvalidOperationException("Cannot manually emit data to a stream with a source operator."); + + // We can only cancel before we queue the work, since operators are synchronous today. + cancellationToken.ThrowIfCancellationRequested(); + + // Dispatch pipeline work off the caller thread. + return Task.Run(() => + { + // If you ever add cooperative cancellation to operators, + // plumb 'cancellationToken' through and honor it there. + _operatorChain.Process(value); + }, cancellationToken); + } + public IReadOnlyDictionary> GetBranches() { var branchDict = new Dictionary>(); diff --git a/src/Cortex.Streams/StreamStatuses.cs b/src/Cortex.Streams/StreamStatuses.cs new file mode 100644 index 0000000..7b0c77d --- /dev/null +++ b/src/Cortex.Streams/StreamStatuses.cs @@ -0,0 +1,8 @@ +namespace Cortex.Streams +{ + public enum StreamStatuses + { + RUNNING, + NOT_RUNNING, + } +}