From 3eb0932a73e34f7df9258fa5d761700d962a78b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Tue, 24 Feb 2026 20:23:56 +0400 Subject: [PATCH 01/24] =?UTF-8?q?=D0=9D=D0=B0=D1=87=D0=B0=D0=BB=D0=BE=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Стыдно должно быть, но пока рано --- Client.Wasm/Components/StudentCard.razor | 8 +- .../Services/EmployeeGenerator.cs | 81 +++++++++++++++++++ CompanyEmployee.Domain/Entity/Employee.cs | 63 +++++++++++++++ CompanyEmployee.ServiceDefaults/Extensions.cs | 0 4 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 CompanyEmployee.Api/Services/EmployeeGenerator.cs create mode 100644 CompanyEmployee.Domain/Entity/Employee.cs create mode 100644 CompanyEmployee.ServiceDefaults/Extensions.cs diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 661f1181..419e4ce1 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -4,10 +4,10 @@ - Номер №X "Название лабораторной" - Вариант №Х "Название варианта" - Выполнена Фамилией Именем 65ХХ - Ссылка на форк + Номер №1 "Кэширование" + Вариант №43 "Сотрудник компании" + Выполнена Казаковым Андреем 6513 + Ссылка на форк diff --git a/CompanyEmployee.Api/Services/EmployeeGenerator.cs b/CompanyEmployee.Api/Services/EmployeeGenerator.cs new file mode 100644 index 00000000..0e3c34e8 --- /dev/null +++ b/CompanyEmployee.Api/Services/EmployeeGenerator.cs @@ -0,0 +1,81 @@ +using Bogus; +using CompanyEmployee.Domain.Entities; + +namespace CompanyEmployee.Api.Services; + +public class EmployeeGenerator : IEmployeeGenerator +{ + private readonly ILogger _logger; + private readonly string[] _professions = { "Developer", "Manager", "Analyst", "Designer", "QA" }; + private readonly string[] _suffixes = { "Junior", "Middle", "Senior" }; + + public EmployeeGenerator(ILogger logger) + { + _logger = logger; + } + + /// + /// + /// + public Employee Generate(int? seed = null) + { + if (seed.HasValue) + Randomizer.Seed = new Random(seed.Value); + + var faker = new Faker(); + + var gender = faker.PickRandom(); + var firstName = faker.Name.FirstName(gender); + var lastName = faker.Name.LastName(gender); + var patronymicBase = faker.Name.FirstName(Name.Gender.Male); + string patronymic = gender == Name.Gender.Male + ? patronymicBase + "" + : patronymicBase + ""; + var fullName = $"{lastName} {firstName} {patronymic}"; + + var profession = faker.PickRandom(_professions); + var suffix = faker.PickRandom(_suffixes); + var position = $"{profession} {suffix}".Trim(); + + var department = faker.Commerce.Department(); + + var hireDate = DateOnly.FromDateTime(faker.Date.Past(10).ToUniversalTime()); + + decimal salary = suffix switch + { + "Junior" => faker.Random.Decimal(30000, 60000), + "Middle" => faker.Random.Decimal(60000, 100000), + "Senior" => faker.Random.Decimal(100000, 180000), + _ => faker.Random.Decimal(40000, 80000) + }; + salary = Math.Round(salary, 2); + + var email = faker.Internet.Email(firstName, lastName); + var phone = faker.Phone.PhoneNumber("+7(###)###-##-##"); + var isTerminated = faker.Random.Bool(0.1f); + + DateOnly? terminationDate = null; + if (isTerminated) + { + var termDate = faker.Date.Between(hireDate.ToDateTime(TimeOnly.MinValue), DateTime.Now); + terminationDate = DateOnly.FromDateTime(termDate); + } + + var employee = new Employee + { + Id = faker.Random.Int(1, 100000), + FullName = fullName, + Position = position, + Department = department, + HireDate = hireDate, + Salary = salary, + Email = email, + Phone = phone, + IsTerminated = isTerminated, + TerminationDate = terminationDate + }; + + _logger.LogInformation(" : {@Employee}", employee); + return employee; + } +} \ No newline at end of file diff --git a/CompanyEmployee.Domain/Entity/Employee.cs b/CompanyEmployee.Domain/Entity/Employee.cs new file mode 100644 index 00000000..0ee49eb3 --- /dev/null +++ b/CompanyEmployee.Domain/Entity/Employee.cs @@ -0,0 +1,63 @@ +namespace CompanyEmployee.Domain.Entities; + +public class Employee +{ + /// + /// + /// + public int Id { get; set; } + + + /// + /// + /// + public string FullName { get; set; } = string.Empty; + + + /// + /// + /// + public string Position { get; set; } = string.Empty; + + + /// + /// + /// + public string Department { get; set; } = string.Empty; + + + /// + /// + /// + public DateOnly HireDate { get; set; } + + + /// + /// + /// + public decimal Salary { get; set; } + + + /// + /// + /// + public string Email { get; set; } = string.Empty; + + + /// + /// + /// + public string Phone { get; set; } = string.Empty; + + + /// + /// + /// + public bool IsTerminated { get; set; } + + + /// + /// + /// + public DateOnly? TerminationDate { get; set; } +} \ No newline at end of file diff --git a/CompanyEmployee.ServiceDefaults/Extensions.cs b/CompanyEmployee.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..e69de29b From 27f78675c74892e64933086e6786a7c980e2e51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Tue, 24 Feb 2026 20:27:03 +0400 Subject: [PATCH 02/24] =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/EmployeeGenerator.cs | Bin 2767 -> 5696 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/CompanyEmployee.Api/Services/EmployeeGenerator.cs b/CompanyEmployee.Api/Services/EmployeeGenerator.cs index 0e3c34e846fd4c2c90366776a9f667a869df4eec..a2ea536ef3ef18ec1a8f8f19f3e35e28857e2006 100644 GIT binary patch literal 5696 zcmbtYTXWk)6h04nVumOFfG~Y5DWr981RDCnFda9s6A!+PZ3xEWzYhJr zT_5ePw3ZYYjlA++&i#DnNdNxxhb&|$6SEp`ed$Zm%v@)zuWQk|q{hS_YPC#L7=1A)tPpQ<=J_6q%c)Y>xx%`P|o>M7F zbp~1A%gC&xSRH}(0#{m+^eIkG44odNn}S{{4{>)J>?EhPu}q2+yk+u|G6QiNy{tEvuaE-8AC1NzWeNHlZ*-7WeRWa(zvDE&L{`S4S z7erVXZz@+9A69#xMi?<0L($jkj_^tmkF?|%R}a2gNhjc~yFN+2Ob&5BPWF@Iq=PY) zBaFv*dXhZA+ClOVYjF;DDtUyNTeNU0p^f{}h9`+F-$D8XJlKauc`CN$&q5<2;E-Ha0} z8xfdQUQHyw4{^V18zJ`T2bkh8f$Y?YKOp}R-wKq9T#&JB~(LegIkbi4SR-O z-+=epRGBdD?0ihB4d~S8V5}CG<06)eZFB~)(~H_%^DAa^Uj8Z8^|~CJA?8{A+4nK} zee1^LwwCm{(1MwqA$x|%9ETyXh*O_3C+Lki_!5a&AIdt~E|cqX(?1f+%IBJKyn=M> z^(-1{R_nhXAp!d$Vir+gobNj`TCs}x2%zVPzcvEfa#!|YZCYX~zknXQOiI4MjuB>Q z9n#s$eG;<}Epu-6@)Rp3M_cQyg?!s{{TjQMICB-v%5`*BbF3v{??n#mW_?}#%3?j> zyhlrAeiR1AH%26k?6*r$34F`o%DOm&Y^WnZp+sgKQq_f zcKpJO_P3SWK`pAI+F%IVof&%UAtuJUuD|pp3bTsu@l$BrhMzOU@*w9F^RJ?hq895n z-)85qsdBYwt~)JqXuoaioyxAGcG$ag2)lGWRgEud7nLrQ$G3;BWazsXA=mh2U1-B| zA#z)U)%^}v^+~P5j*HfK3oqy!0^_Q9lKt=${C35cYN|R^Sy*X18FtqbrKe|h@h`s% zszhpCX5I`s>bD*Bwte3yzM;h;s@jX){Ef}`SQx=wSb7O>-8Ekze0;1!yO?5cQ%xh*TUblRx|M2L&k8YX=@cWmn#LOww_)XXy+HL@hpv`0BgCR@ z^*M))_S`JT&FXXAO5EFpDAhF28X%hHP&_ANk*>8dwibS)$+q7C8QBBG7txi~(jqJC z#iz;r{5#75zO3;5XdmBJU_0~tBKaoz-Gy&RYk6GyvdB)F2vyAIUC6+Gc8NS;RV|+i KQQo4+Mm_*=tT*KV literal 2767 zcmai0&2A$_5Wf2c3L~rJ?fru)@%9ccmm!a z3ak{6Y=z~(feTgL^KToh#O~>?uYSL(>C^~0W~XYLn*CO4AHGsi%;oevibFL82+mZ* zg$&MRA`$^8){;B|6Y~MErtC>9f@{zdF|dMh`Z^Q?HVC;fELZq7NYFe{n(eXf%4TcU zV)%)*nD7Lc20m0WoU-mqH6DXLSOFZdo6sKZuLqh$V~9Wg#BO4(MqrFk(y&7|W8N7| zAXG7Eufx0^mmCey@ACc1=DjXYMG=Dha-j$uTnkL$oZ@Fl_oIM$D(<-~~57@Gu1@ zKrYkg6_-O5iFcrpJWdJRWxEdNKv}*BGU1wy_#4n<5rr>k?YA2eV~|5DVnu^1F?i!_ z9azQ(XFTE8keJ{S8699aD1Ag)$OL_?8KF%=0%*$VvCiQkgHvucnydI4^mqvEeOCDE$j(Yd=yRTgL)JtVFbeg!g0kkHF>m?s__tS++FL8N zxc3@4LlR^u`^cN+xtO!rD9;+MOE2dLFU(NR%T_ItUKY=g)y`dUOQ{e(%-cfn!=UW_Z!G%^4 zJ@iEczT+4@23JTIe|M)H^wsxLOh6kRTA4NmQ6y{#0}=6%;UTs*>dlg1?nE-UEyT(( zLFM+c$;1wiGFH3JK-aA{|M?DnI_xw0X?F@O7Ta^PY$Mi!I&7D)E8u+mZ8Gdoi2h;? z`eviePf5DN9H?+No7d!Zk)9q;Zi81Ul|$d{b=W7>d=dcx-+5tC*RD)JOGtuq+V%?) zcUVsCCaG?fEF1WWo&x)QnMSX1PN(1T2lvHSW3dtRS^m8gBJxRzi zlfEpcdqTRY-LX8U{oqpFRpO+gtU8+qQg4dfkqSI}t&tX2F?U~~c=3ghzoZH#)Pw7; zWEE@d?{47iMLEt!*|OddSLG1N=Sf|l90=%NIXUV=FUrx7r?xI|U39oz)DJB;QM;vX z{m4=ek76QX{is_fT9p`e!Tt)Z8nr7jo!@JyGTDtJz}s6_j+Bl#&0634 Date: Tue, 24 Feb 2026 20:28:42 +0400 Subject: [PATCH 03/24] =?UTF-8?q?=D0=B5=D1=89=D0=B5=20=D1=82=D0=B5=D1=81?= =?UTF-8?q?=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CompanyEmployee.Domain/Entity/Employee.cs | Bin 1166 -> 2458 bytes CompanyEmployee.ServiceDefaults/Extensions.cs | Bin 0 -> 6958 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/CompanyEmployee.Domain/Entity/Employee.cs b/CompanyEmployee.Domain/Entity/Employee.cs index 0ee49eb3455667d093f671d87f15381b5cb0d1e8..9c51c1df9cdb66728d167e2381cc10b7eea3e4f0 100644 GIT binary patch literal 2458 zcmc(h-%A2P5Xa|Xo`T+je2OD_tt2ue^Q^RA5EaxvpcW=v8t3&8(Z61OX7q~8r1DIT z)p2KcZ$JB;nf3LhBXzlx8|leCRue0bs&whI-yM0B1Ggjk9`WADn$+B1mp1-2{2fWK zt=T#ynU^_l_wpbOiRf>#Y7?uDzlS}%rpF!$_wW^Thh}^D`u6p)@Bd6_=aI0+iCpvf zL~C&(2`3V;N@_j7pU>zw{)Bf$bGnqlj(hAbb{$*n@sN{_c+NO$i-~azfs~!KUi8#cJBVR)U}8!D^(LqLTz{;7*sWrm zvy9GV&%y#XiE(F5c_FvdoesLrmNEz1=FnYP)eWz@r}*qXEu#43{0(@!uIdT(_6qVC prR}!tXX~_Y%Y{KU^3P@}d}(b-t4;-Fhbh~xc{*~O@^;?O@&;PAOSb?3 literal 1166 zcmbu8L2JS=6vyxV6z>z*51`u)(=jibuzi78g9{{SY3iYf-%gk(4`bIs5i^Q4wN_yl z!9}K)ZelIVp^)Ug_v`!T|2Se;ILN|qYcmINzrh@8`xsa1HbaD08r&lu!J=vyPOnXg z1r|j@z#-1)8=#kInotux#*ph@A5TV8snT#A&Vn>fU>T>$YO3@OCa?^bshrDWXwA%J z?^%R<(Ck1Ty4b6N*nI|uktOAFcK`4`8fUjBOA5~=ybE_dN*{Fd`Py&=Klc^gpXckQ zvub}>!#Mg>@iN_HG3SUaTWBS1{tx0foyzDkS{*yc^%&Ly=F}_KVUPx}NJ2Re3aEAD z;YUt=xF;^&ZSvH!I=%+UBGRM+LTTdHD6rP89s+6}pw$g?tBULUXiDCgJm`emI;1n!Tsj4btn@dCNm3N)gs`%?| zzi-Be-C579v7t~|_Rh}boH@7eoEiV~_edq`TJ_bXiqsKW9W_(~^bOR7N>x?usXy`T zVFu?7%(JR?OzQ$YmuiF+ZOpNw>*|I227QZA-36c6@amf1jvA{2aQp*rok!uYZ)D?M zDe?4Pn-l&maJ<3yGrlS08$%OElQwmf%(=E@bkqR_%ac0lLu;g;fM**tdvjU9Ckd&> z(ET~~&Rs^Jolb1Sw+{N-=u6P{*_G@6GWLrwgP$MFCsG?&Gs12^V?DL8>7DgbZ|WcQ zieA<0x~bplZ5^owephr|zrpVo+I9UCM%e!r@2k3|8|aCk-(&2l1)dG?in0G8G_r>G zuuZmU^SF(;8K; zj2qBMnJ&uof1t!_4M-SItdzyhHgwEA(EEqjSp%)EIyG-117qX@5|50`iK-aMDOH46 znUDUt!*AJ<*YyMO2Y1CZ4)jW_RZ+_m?-y$FEK;>{yOQohZn5)D&dT?v>gq9pef|0^ zddNdK6mhZ#3#ItPKv40uRJoGUrdN|$k*XJBO?2kBkBrLtOh)2BAKt|H?Wz-Kv*_>p zlNrRlA@&fmR+vXV)^?{NEllw?$cu5A!b^wPdjwp|$A?8y+0Cu-ULu(E%!ggy0iTFf zTX@!hODhxn3ZqY?-WZ&V)H-nQHCAom$(41qo0|BuVW@23eGMo>q_G?t;3f0d(0YEX znfBDMy8jk99Wm~yF9O=ZH2Izl}4)Ccewn*7N4?xBzM0&{H} zdu8sag@Q zTQNVo6;#DkKh%h=sKRy;O68a*vshDQRK-6|p!YuduMJmLm8^|u4bJCCmWPQ{K3xe1 z|5G?^gC?_hYCOcsOk^akKZLF?bIg{b+|%eAWs+vLYe525sW$3}0QvXFvfVXmn+SR; zdgUH0#0rRKlm4B>7=ww)o~-5=HSygTbzLr-Sn+;dC9_?*ZM>w28rJfx=xpxnIiJcecagXH|623OS$y?}I+?}m(Y(;&HJ*DR4#;iKqbISa}&tD?ozw%V8 zDc`<|{$k}0V-`<8u|aHNwZx|15>Hoy_|8v9s2m#lJt_;Ho_@wJPfKCR7(Gp#qYl+a z_+Jeb3agGatgnO4Cf*P6zKZWVR4VrTRnr?7VfDkgn|R8KUG%)xJEki7fObtUFMKW{ z&VMl|%WUYLOy@nhEMi|~pr<+~QBI!Vcn`ueQ3@Q0&6zvzbvApyGP z@M(g|nb|k}^b|DgO~Eum7VEbxoBgVmj7RLjCw|8set~B+aRO+eeS#kG7LFYQO*wji zRtxKXyTd16)CpRbOX)}9a?~xx^Yq!RC~psU6yc~iM69!BWUOl5ycQ26faDRbIv^s;*o@7A9Q#8)JCv)jE2G7w|$2B(50bDv8ul*sq< zIl+A1&+`0L#cf-5Yd8ijhU@L8^VDxqcfH4_X6mevj~U c^Ahgq^ZHu4t7OFA Date: Tue, 24 Feb 2026 21:18:42 +0400 Subject: [PATCH 04/24] =?UTF-8?q?=D0=9E=D0=B1=D0=BB=D0=B0=D0=B6=D0=B0?= =?UTF-8?q?=D0=BB=D1=81=D1=8F....?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Не судите строго, я pycharman --- .../Services/EmployeeGenerator.cs | Bin 5696 -> 0 bytes CompanyEmployee.Domain/Entity/Employee.cs | Bin 2458 -> 0 bytes CompanyEmployee.ServiceDefaults/Extensions.cs | Bin 6958 -> 0 bytes 3 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 CompanyEmployee.Api/Services/EmployeeGenerator.cs delete mode 100644 CompanyEmployee.Domain/Entity/Employee.cs delete mode 100644 CompanyEmployee.ServiceDefaults/Extensions.cs diff --git a/CompanyEmployee.Api/Services/EmployeeGenerator.cs b/CompanyEmployee.Api/Services/EmployeeGenerator.cs deleted file mode 100644 index a2ea536ef3ef18ec1a8f8f19f3e35e28857e2006..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5696 zcmbtYTXWk)6h04nVumOFfG~Y5DWr981RDCnFda9s6A!+PZ3xEWzYhJr zT_5ePw3ZYYjlA++&i#DnNdNxxhb&|$6SEp`ed$Zm%v@)zuWQk|q{hS_YPC#L7=1A)tPpQ<=J_6q%c)Y>xx%`P|o>M7F zbp~1A%gC&xSRH}(0#{m+^eIkG44odNn}S{{4{>)J>?EhPu}q2+yk+u|G6QiNy{tEvuaE-8AC1NzWeNHlZ*-7WeRWa(zvDE&L{`S4S z7erVXZz@+9A69#xMi?<0L($jkj_^tmkF?|%R}a2gNhjc~yFN+2Ob&5BPWF@Iq=PY) zBaFv*dXhZA+ClOVYjF;DDtUyNTeNU0p^f{}h9`+F-$D8XJlKauc`CN$&q5<2;E-Ha0} z8xfdQUQHyw4{^V18zJ`T2bkh8f$Y?YKOp}R-wKq9T#&JB~(LegIkbi4SR-O z-+=epRGBdD?0ihB4d~S8V5}CG<06)eZFB~)(~H_%^DAa^Uj8Z8^|~CJA?8{A+4nK} zee1^LwwCm{(1MwqA$x|%9ETyXh*O_3C+Lki_!5a&AIdt~E|cqX(?1f+%IBJKyn=M> z^(-1{R_nhXAp!d$Vir+gobNj`TCs}x2%zVPzcvEfa#!|YZCYX~zknXQOiI4MjuB>Q z9n#s$eG;<}Epu-6@)Rp3M_cQyg?!s{{TjQMICB-v%5`*BbF3v{??n#mW_?}#%3?j> zyhlrAeiR1AH%26k?6*r$34F`o%DOm&Y^WnZp+sgKQq_f zcKpJO_P3SWK`pAI+F%IVof&%UAtuJUuD|pp3bTsu@l$BrhMzOU@*w9F^RJ?hq895n z-)85qsdBYwt~)JqXuoaioyxAGcG$ag2)lGWRgEud7nLrQ$G3;BWazsXA=mh2U1-B| zA#z)U)%^}v^+~P5j*HfK3oqy!0^_Q9lKt=${C35cYN|R^Sy*X18FtqbrKe|h@h`s% zszhpCX5I`s>bD*Bwte3yzM;h;s@jX){Ef}`SQx=wSb7O>-8Ekze0;1!yO?5cQ%xh*TUblRx|M2L&k8YX=@cWmn#LOww_)XXy+HL@hpv`0BgCR@ z^*M))_S`JT&FXXAO5EFpDAhF28X%hHP&_ANk*>8dwibS)$+q7C8QBBG7txi~(jqJC z#iz;r{5#75zO3;5XdmBJU_0~tBKaoz-Gy&RYk6GyvdB)F2vyAIUC6+Gc8NS;RV|+i KQQo4+Mm_*=tT*KV diff --git a/CompanyEmployee.Domain/Entity/Employee.cs b/CompanyEmployee.Domain/Entity/Employee.cs deleted file mode 100644 index 9c51c1df9cdb66728d167e2381cc10b7eea3e4f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2458 zcmc(h-%A2P5Xa|Xo`T+je2OD_tt2ue^Q^RA5EaxvpcW=v8t3&8(Z61OX7q~8r1DIT z)p2KcZ$JB;nf3LhBXzlx8|leCRue0bs&whI-yM0B1Ggjk9`WADn$+B1mp1-2{2fWK zt=T#ynU^_l_wpbOiRf>#Y7?uDzlS}%rpF!$_wW^Thh}^D`u6p)@Bd6_=aI0+iCpvf zL~C&(2`3V;N@_j7pU>zw{)Bf$bGnqlj(hAbb{$*n@sN{_c+NO$i-~azfs~!KUi8#cJBVR)U}8!D^(LqLTz{;7*sWrm zvy9GV&%y#XiE(F5c_FvdoesLrmNEz1=FnYP)eWz@r}*qXEu#43{0(@!uIdT(_6qVC prR}!tXX~_Y%Y{KU^3P@}d}(b-t4;-Fhbh~xc{*~O@^;?O@&;PAOSb?3 diff --git a/CompanyEmployee.ServiceDefaults/Extensions.cs b/CompanyEmployee.ServiceDefaults/Extensions.cs deleted file mode 100644 index 2f151702323577ff693fd66420687f8c40d3adae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6958 zcmd5>TTdHD6rP89s+6}pw$g?tBULUXiDCgJm`emI;1n!Tsj4btn@dCNm3N)gs`%?| zzi-Be-C579v7t~|_Rh}boH@7eoEiV~_edq`TJ_bXiqsKW9W_(~^bOR7N>x?usXy`T zVFu?7%(JR?OzQ$YmuiF+ZOpNw>*|I227QZA-36c6@amf1jvA{2aQp*rok!uYZ)D?M zDe?4Pn-l&maJ<3yGrlS08$%OElQwmf%(=E@bkqR_%ac0lLu;g;fM**tdvjU9Ckd&> z(ET~~&Rs^Jolb1Sw+{N-=u6P{*_G@6GWLrwgP$MFCsG?&Gs12^V?DL8>7DgbZ|WcQ zieA<0x~bplZ5^owephr|zrpVo+I9UCM%e!r@2k3|8|aCk-(&2l1)dG?in0G8G_r>G zuuZmU^SF(;8K; zj2qBMnJ&uof1t!_4M-SItdzyhHgwEA(EEqjSp%)EIyG-117qX@5|50`iK-aMDOH46 znUDUt!*AJ<*YyMO2Y1CZ4)jW_RZ+_m?-y$FEK;>{yOQohZn5)D&dT?v>gq9pef|0^ zddNdK6mhZ#3#ItPKv40uRJoGUrdN|$k*XJBO?2kBkBrLtOh)2BAKt|H?Wz-Kv*_>p zlNrRlA@&fmR+vXV)^?{NEllw?$cu5A!b^wPdjwp|$A?8y+0Cu-ULu(E%!ggy0iTFf zTX@!hODhxn3ZqY?-WZ&V)H-nQHCAom$(41qo0|BuVW@23eGMo>q_G?t;3f0d(0YEX znfBDMy8jk99Wm~yF9O=ZH2Izl}4)Ccewn*7N4?xBzM0&{H} zdu8sag@Q zTQNVo6;#DkKh%h=sKRy;O68a*vshDQRK-6|p!YuduMJmLm8^|u4bJCCmWPQ{K3xe1 z|5G?^gC?_hYCOcsOk^akKZLF?bIg{b+|%eAWs+vLYe525sW$3}0QvXFvfVXmn+SR; zdgUH0#0rRKlm4B>7=ww)o~-5=HSygTbzLr-Sn+;dC9_?*ZM>w28rJfx=xpxnIiJcecagXH|623OS$y?}I+?}m(Y(;&HJ*DR4#;iKqbISa}&tD?ozw%V8 zDc`<|{$k}0V-`<8u|aHNwZx|15>Hoy_|8v9s2m#lJt_;Ho_@wJPfKCR7(Gp#qYl+a z_+Jeb3agGatgnO4Cf*P6zKZWVR4VrTRnr?7VfDkgn|R8KUG%)xJEki7fObtUFMKW{ z&VMl|%WUYLOy@nhEMi|~pr<+~QBI!Vcn`ueQ3@Q0&6zvzbvApyGP z@M(g|nb|k}^b|DgO~Eum7VEbxoBgVmj7RLjCw|8set~B+aRO+eeS#kG7LFYQO*wji zRtxKXyTd16)CpRbOX)}9a?~xx^Yq!RC~psU6yc~iM69!BWUOl5ycQ26faDRbIv^s;*o@7A9Q#8)JCv)jE2G7w|$2B(50bDv8ul*sq< zIl+A1&+`0L#cf-5Yd8ijhU@L8^VDxqcfH4_X6mevj~U c^Ahgq^ZHu4t7OFA Date: Tue, 24 Feb 2026 22:06:39 +0400 Subject: [PATCH 05/24] =?UTF-8?q?=D0=98=D1=81=D0=BF=D0=B0=D1=80=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CloudDevelopment.sln | 24 ++++ .../CompanyEmployee.Api.csproj | 20 +++ CompanyEmployee.Api/CompanyEmployee.Api.http | 6 + .../Controllers/WeatherForecastController.cs | 32 +++++ .../Properties/launchSettings.json | 41 ++++++ .../Services/EmployeeGenerator.cs | 83 ++++++++++++ .../Services/IEmployeeGenerator.cs | 11 ++ .../appsettings.Development.json | 8 ++ CompanyEmployee.Api/appsettings.json | 9 ++ CompanyEmployee.AppHost/AppHost.cs | 5 + .../CompanyEmployee.AppHost.csproj | 21 +++ .../Properties/launchSettings.json | 29 ++++ .../appsettings.Development.json | 8 ++ CompanyEmployee.AppHost/appsettings.json | 9 ++ .../CompanyEmployee.Domain.csproj | 10 ++ CompanyEmployee.Domain/Entity/Employee.cs | 63 +++++++++ .../CompanyEmployee.ServiceDefaults.csproj | 22 +++ CompanyEmployee.ServiceDefaults/Extensions.cs | 127 ++++++++++++++++++ 18 files changed, 528 insertions(+) create mode 100644 CompanyEmployee.Api/CompanyEmployee.Api.csproj create mode 100644 CompanyEmployee.Api/CompanyEmployee.Api.http create mode 100644 CompanyEmployee.Api/Controllers/WeatherForecastController.cs create mode 100644 CompanyEmployee.Api/Properties/launchSettings.json create mode 100644 CompanyEmployee.Api/Services/EmployeeGenerator.cs create mode 100644 CompanyEmployee.Api/Services/IEmployeeGenerator.cs create mode 100644 CompanyEmployee.Api/appsettings.Development.json create mode 100644 CompanyEmployee.Api/appsettings.json create mode 100644 CompanyEmployee.AppHost/AppHost.cs create mode 100644 CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj create mode 100644 CompanyEmployee.AppHost/Properties/launchSettings.json create mode 100644 CompanyEmployee.AppHost/appsettings.Development.json create mode 100644 CompanyEmployee.AppHost/appsettings.json create mode 100644 CompanyEmployee.Domain/CompanyEmployee.Domain.csproj create mode 100644 CompanyEmployee.Domain/Entity/Employee.cs create mode 100644 CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj create mode 100644 CompanyEmployee.ServiceDefaults/Extensions.cs diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index cb48241d..3a39d831 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -5,6 +5,14 @@ VisualStudioVersion = 17.14.36811.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.AppHost", "CompanyEmployee.AppHost\CompanyEmployee.AppHost.csproj", "{069756DA-EFFA-4835-B69C-0849C48BE473}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.ServiceDefaults", "CompanyEmployee.ServiceDefaults\CompanyEmployee.ServiceDefaults.csproj", "{60C547C0-C951-4270-1D2E-4BB68A5739B6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.Domain", "CompanyEmployee.Domain\CompanyEmployee.Domain.csproj", "{FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.Api", "CompanyEmployee.Api\CompanyEmployee.Api.csproj", "{EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +23,22 @@ Global {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Debug|Any CPU.Build.0 = Debug|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Release|Any CPU.ActiveCfg = Release|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Release|Any CPU.Build.0 = Release|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Release|Any CPU.Build.0 = Release|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Release|Any CPU.Build.0 = Release|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CompanyEmployee.Api/CompanyEmployee.Api.csproj b/CompanyEmployee.Api/CompanyEmployee.Api.csproj new file mode 100644 index 00000000..683c5a13 --- /dev/null +++ b/CompanyEmployee.Api/CompanyEmployee.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/CompanyEmployee.Api/CompanyEmployee.Api.http b/CompanyEmployee.Api/CompanyEmployee.Api.http new file mode 100644 index 00000000..4a6b1c11 --- /dev/null +++ b/CompanyEmployee.Api/CompanyEmployee.Api.http @@ -0,0 +1,6 @@ +@CompanyEmployee.Api_HostAddress = http://localhost:5121 + +GET {{CompanyEmployee.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/CompanyEmployee.Api/Controllers/WeatherForecastController.cs b/CompanyEmployee.Api/Controllers/WeatherForecastController.cs new file mode 100644 index 00000000..aa93f318 --- /dev/null +++ b/CompanyEmployee.Api/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; + +namespace CompanyEmployee.Api.Controllers; + +[ApiController] +[Route("[controller]")] +public class WeatherForecastController : ControllerBase +{ + private static readonly string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + private readonly ILogger _logger; + + public WeatherForecastController(ILogger logger) + { + _logger = logger; + } + + [HttpGet(Name = "GetWeatherForecast")] + public IEnumerable Get() + { + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), + TemperatureC = Random.Shared.Next(-20, 55), + Summary = Summaries[Random.Shared.Next(Summaries.Length)] + }) + .ToArray(); + } +} diff --git a/CompanyEmployee.Api/Properties/launchSettings.json b/CompanyEmployee.Api/Properties/launchSettings.json new file mode 100644 index 00000000..2db4f09e --- /dev/null +++ b/CompanyEmployee.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:56739", + "sslPort": 44378 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5121", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7106;http://localhost:5121", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/CompanyEmployee.Api/Services/EmployeeGenerator.cs b/CompanyEmployee.Api/Services/EmployeeGenerator.cs new file mode 100644 index 00000000..ef7b6c33 --- /dev/null +++ b/CompanyEmployee.Api/Services/EmployeeGenerator.cs @@ -0,0 +1,83 @@ +using Bogus; +using Bogus.DataSets; +using CompanyEmployee.Domain.Entity; +using System.Xml.Linq; + +namespace CompanyEmployee.Api.Services; + +public class EmployeeGenerator : IEmployeeGenerator +{ + private readonly ILogger _logger; + private readonly string[] _professions = { "Developer", "Manager", "Analyst", "Designer", "QA" }; + private readonly string[] _suffixes = { "Junior", "Middle", "Senior" }; + + public EmployeeGenerator(ILogger logger) + { + _logger = logger; + } + + /// + /// Генератор сотрудника + /// + public Employee Generate(int? seed = null) + { + if (seed.HasValue) + Randomizer.Seed = new Random(seed.Value); + + var faker = new Faker(); + + var gender = faker.PickRandom(); + var firstName = faker.Name.FirstName(gender); + var lastName = faker.Name.LastName(gender); + var patronymicBase = faker.Name.FirstName(Name.Gender.Male); + var patronymic = gender == Name.Gender.Male + ? patronymicBase + "ович" + : patronymicBase + "овна"; + var fullName = $"{lastName} {firstName} {patronymic}"; + + var profession = faker.PickRandom(_professions); + var suffix = faker.PickRandom(_suffixes); + var position = $"{profession} {suffix}".Trim(); + + var department = faker.Commerce.Department(); + + var hireDate = DateOnly.FromDateTime(faker.Date.Past(10).ToUniversalTime()); + + var salary = suffix switch + { + "Junior" => faker.Random.Decimal(30000, 60000), + "Middle" => faker.Random.Decimal(60000, 100000), + "Senior" => faker.Random.Decimal(100000, 180000), + _ => faker.Random.Decimal(40000, 80000) + }; + salary = Math.Round(salary, 2); + + var email = faker.Internet.Email(firstName, lastName); + var phone = faker.Phone.PhoneNumber("+7(###)###-##-##"); + var isTerminated = faker.Random.Bool(0.1f); + + DateOnly? terminationDate = null; + if (isTerminated) + { + var termDate = faker.Date.Between(hireDate.ToDateTime(TimeOnly.MinValue), DateTime.Now); + terminationDate = DateOnly.FromDateTime(termDate); + } + + var employee = new Employee + { + Id = faker.Random.Int(1, 100000), + FullName = fullName, + Position = position, + Department = department, + HireDate = hireDate, + Salary = salary, + Email = email, + Phone = phone, + IsTerminated = isTerminated, + TerminationDate = terminationDate + }; + + _logger.LogInformation("Сгенерирован новый сотрудник: {@Employee}", employee); + return employee; + } +} \ No newline at end of file diff --git a/CompanyEmployee.Api/Services/IEmployeeGenerator.cs b/CompanyEmployee.Api/Services/IEmployeeGenerator.cs new file mode 100644 index 00000000..b0b4e31d --- /dev/null +++ b/CompanyEmployee.Api/Services/IEmployeeGenerator.cs @@ -0,0 +1,11 @@ +using CompanyEmployee.Domain.Entity; + +namespace CompanyEmployee.Api.Services; + +public interface IEmployeeGenerator +{ + /// + /// Генерирует нового сотрудника. + /// + public Employee Generate(int? seed = null); +} \ No newline at end of file diff --git a/CompanyEmployee.Api/appsettings.Development.json b/CompanyEmployee.Api/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/CompanyEmployee.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CompanyEmployee.Api/appsettings.json b/CompanyEmployee.Api/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/CompanyEmployee.Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs new file mode 100644 index 00000000..9b9aac1a --- /dev/null +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -0,0 +1,5 @@ +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddProject("companyemployee-api"); + +builder.Build().Run(); diff --git a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj new file mode 100644 index 00000000..909c1aad --- /dev/null +++ b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj @@ -0,0 +1,21 @@ + + + + + + Exe + net8.0 + enable + enable + 78f7593b-e0aa-4c9f-9165-d722e0a4dde4 + + + + + + + + + + + diff --git a/CompanyEmployee.AppHost/Properties/launchSettings.json b/CompanyEmployee.AppHost/Properties/launchSettings.json new file mode 100644 index 00000000..6c680ac5 --- /dev/null +++ b/CompanyEmployee.AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17057;http://localhost:15121", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21201", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22004" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15121", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19103", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20107" + } + } + } +} diff --git a/CompanyEmployee.AppHost/appsettings.Development.json b/CompanyEmployee.AppHost/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/CompanyEmployee.AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CompanyEmployee.AppHost/appsettings.json b/CompanyEmployee.AppHost/appsettings.json new file mode 100644 index 00000000..31c092aa --- /dev/null +++ b/CompanyEmployee.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/CompanyEmployee.Domain/CompanyEmployee.Domain.csproj b/CompanyEmployee.Domain/CompanyEmployee.Domain.csproj new file mode 100644 index 00000000..2150e379 --- /dev/null +++ b/CompanyEmployee.Domain/CompanyEmployee.Domain.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/CompanyEmployee.Domain/Entity/Employee.cs b/CompanyEmployee.Domain/Entity/Employee.cs new file mode 100644 index 00000000..86638159 --- /dev/null +++ b/CompanyEmployee.Domain/Entity/Employee.cs @@ -0,0 +1,63 @@ +namespace CompanyEmployee.Domain.Entity; + +public class Employee +{ + /// + /// Идентификатор сотрудника в системе + /// + public int Id { get; set; } + + + /// + /// ФИО + /// + public string FullName { get; set; } = string.Empty; + + + /// + /// Должность + /// + public string Position { get; set; } = string.Empty; + + + /// + /// Отдел + /// + public string Department { get; set; } = string.Empty; + + + /// + /// Дата приема + /// + public DateOnly HireDate { get; set; } + + + /// + /// Зарплата + /// + public decimal Salary { get; set; } + + + /// + /// Электронная почта + /// + public string Email { get; set; } = string.Empty; + + + /// + /// Телефон + /// + public string Phone { get; set; } = string.Empty; + + + /// + /// Индикатор увольнения + /// + public bool IsTerminated { get; set; } + + + /// + /// Дата увольнения + /// + public DateOnly? TerminationDate { get; set; } +} \ No newline at end of file diff --git a/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj b/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj new file mode 100644 index 00000000..d808731c --- /dev/null +++ b/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + diff --git a/CompanyEmployee.ServiceDefaults/Extensions.cs b/CompanyEmployee.ServiceDefaults/Extensions.cs new file mode 100644 index 00000000..112c1281 --- /dev/null +++ b/CompanyEmployee.ServiceDefaults/Extensions.cs @@ -0,0 +1,127 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + private const string HealthEndpointPath = "/health"; + private const string AlivenessEndpointPath = "/alive"; + + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation(tracing => + // Exclude health check requests from tracing + tracing.Filter = context => + !context.Request.Path.StartsWithSegments(HealthEndpointPath) + && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath) + ) + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks(HealthEndpointPath); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} From 58725bb76ce2fe907fded0f6edd564691f527ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Tue, 24 Feb 2026 22:07:37 +0400 Subject: [PATCH 06/24] Delete WeatherForecastController.cs --- .../Controllers/WeatherForecastController.cs | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 CompanyEmployee.Api/Controllers/WeatherForecastController.cs diff --git a/CompanyEmployee.Api/Controllers/WeatherForecastController.cs b/CompanyEmployee.Api/Controllers/WeatherForecastController.cs deleted file mode 100644 index aa93f318..00000000 --- a/CompanyEmployee.Api/Controllers/WeatherForecastController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace CompanyEmployee.Api.Controllers; - -[ApiController] -[Route("[controller]")] -public class WeatherForecastController : ControllerBase -{ - private static readonly string[] Summaries = new[] - { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) - { - _logger = logger; - } - - [HttpGet(Name = "GetWeatherForecast")] - public IEnumerable Get() - { - return Enumerable.Range(1, 5).Select(index => new WeatherForecast - { - Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = Summaries[Random.Shared.Next(Summaries.Length)] - }) - .ToArray(); - } -} From 06de5f2dbf65737b287123b4a8ceb7bde5b6634a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Tue, 24 Feb 2026 22:24:53 +0400 Subject: [PATCH 07/24] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B4=D0=BE=D0=BB?= =?UTF-8?q?=D0=B6=D0=B0=D1=8E=20=D0=BD=D0=B5=20=D1=81=D0=BF=D0=B0=D1=82?= =?UTF-8?q?=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit С каждой строчкой я все дальше от бога --- .../Controllers/EmployeeController.cs | 5 ++ .../Services/EmployeeService.cs | 54 +++++++++++++++++++ .../Services/IEmployeeService.cs | 11 ++++ 3 files changed, 70 insertions(+) create mode 100644 CompanyEmployee.Api/Controllers/EmployeeController.cs create mode 100644 CompanyEmployee.Api/Services/EmployeeService.cs create mode 100644 CompanyEmployee.Api/Services/IEmployeeService.cs diff --git a/CompanyEmployee.Api/Controllers/EmployeeController.cs b/CompanyEmployee.Api/Controllers/EmployeeController.cs new file mode 100644 index 00000000..3db94ff0 --- /dev/null +++ b/CompanyEmployee.Api/Controllers/EmployeeController.cs @@ -0,0 +1,5 @@ +namespace CompanyEmployee.Api.Controllers; + +public class EmployeeController +{ +} diff --git a/CompanyEmployee.Api/Services/EmployeeService.cs b/CompanyEmployee.Api/Services/EmployeeService.cs new file mode 100644 index 00000000..d6513074 --- /dev/null +++ b/CompanyEmployee.Api/Services/EmployeeService.cs @@ -0,0 +1,54 @@ +using CompanyEmployee.Domain.Entity; +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace CompanyEmployee.Api.Services; + +/// +/// Реализация сервиса сотрудников с кэшированием в Redis через IDistributedCache. +/// +public class EmployeeService : IEmployeeService +{ + private readonly IEmployeeGenerator _generator; + private readonly IDistributedCache _cache; + private readonly ILogger _logger; + private readonly DistributedCacheEntryOptions _cacheOptions; + + public EmployeeService(IEmployeeGenerator generator, IDistributedCache cache, ILogger logger) + { + _generator = generator; + _cache = cache; + _logger = logger; + _cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) + }; + } + + /// + /// Получает сотрудника. + /// + public async Task GetEmployeeAsync(int? seed, CancellationToken cancellationToken = default) + { + var cacheKey = seed.HasValue ? $"employee:seed:{seed}" : $"employee:random:{Guid.NewGuid()}"; + + _logger.LogDebug("Попытка получения сотрудника из кэша по ключу {CacheKey}", cacheKey); + + var cachedJson = await _cache.GetStringAsync(cacheKey, cancellationToken); + if (cachedJson != null) + { + _logger.LogInformation("Сотрудник найден в кэше по ключу {CacheKey}", cacheKey); + var employee = JsonSerializer.Deserialize(cachedJson); + return employee!; + } + + _logger.LogInformation("Сотрудник не найден в кэше, генерация нового. Seed: {Seed}", seed); + var newEmployee = _generator.Generate(seed); + + var serialized = JsonSerializer.Serialize(newEmployee); + await _cache.SetStringAsync(cacheKey, serialized, _cacheOptions, cancellationToken); + _logger.LogDebug("Сгенерированный сотрудник сохранён в кэш по ключу {CacheKey}", cacheKey); + + return newEmployee; + } +} \ No newline at end of file diff --git a/CompanyEmployee.Api/Services/IEmployeeService.cs b/CompanyEmployee.Api/Services/IEmployeeService.cs new file mode 100644 index 00000000..9bc30923 --- /dev/null +++ b/CompanyEmployee.Api/Services/IEmployeeService.cs @@ -0,0 +1,11 @@ +using CompanyEmployee.Domain.Entity; + +namespace CompanyEmployee.Api.Services; + +public interface IEmployeeService +{ + /// + /// Получает сотрудника. + /// + public Task GetEmployeeAsync(int? seed, CancellationToken cancellationToken = default); +} \ No newline at end of file From 361302a74e7a93416f1b358599ff1ceb9a965739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Tue, 24 Feb 2026 23:32:02 +0400 Subject: [PATCH 08/24] =?UTF-8?q?=D0=AF=20=D1=83=D0=B6=D0=B5=20=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BA=D0=BE=20=D0=BA=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=B3=D0=B0=D0=B4=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Секрет Гудини --- .../CompanyEmployee.Api.csproj | 3 ++ .../Controllers/EmployeeController.cs | 35 +++++++++++++++-- CompanyEmployee.Api/Program.cs | 39 +++++++++++++++++++ CompanyEmployee.AppHost/AppHost.cs | 18 ++++++++- .../CompanyEmployee.AppHost.csproj | 8 +++- .../CompanyEmployee.Domain.csproj | 1 - 6 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 CompanyEmployee.Api/Program.cs diff --git a/CompanyEmployee.Api/CompanyEmployee.Api.csproj b/CompanyEmployee.Api/CompanyEmployee.Api.csproj index 683c5a13..9caa8568 100644 --- a/CompanyEmployee.Api/CompanyEmployee.Api.csproj +++ b/CompanyEmployee.Api/CompanyEmployee.Api.csproj @@ -7,7 +7,10 @@ + + + diff --git a/CompanyEmployee.Api/Controllers/EmployeeController.cs b/CompanyEmployee.Api/Controllers/EmployeeController.cs index 3db94ff0..b01ef5f5 100644 --- a/CompanyEmployee.Api/Controllers/EmployeeController.cs +++ b/CompanyEmployee.Api/Controllers/EmployeeController.cs @@ -1,5 +1,34 @@ -namespace CompanyEmployee.Api.Controllers; +using CompanyEmployee.Api.Services; +using CompanyEmployee.Domain.Entity; +using Microsoft.AspNetCore.Mvc; -public class EmployeeController +namespace CompanyEmployee.Api.Controllers; + +/// +/// Контроллер для работы с сотрудниками. +/// +[ApiController] +[Route("api/[controller]")] +public class EmployeeController : ControllerBase { -} + private readonly IEmployeeService _employeeService; + private readonly ILogger _logger; + + public EmployeeController(IEmployeeService employeeService, ILogger logger) + { + _employeeService = employeeService; + _logger = logger; + } + + /// + /// Получить сгенерированного сотрудника. + /// + [HttpGet] + [ProducesResponseType(typeof(Employee), StatusCodes.Status200OK)] + public async Task> GetEmployee(int? seed, CancellationToken cancellationToken) + { + _logger.LogInformation("Запрос на получение сотрудника с seed: {Seed}", seed); + var employee = await _employeeService.GetEmployeeAsync(seed, cancellationToken); + return Ok(employee); + } +} \ No newline at end of file diff --git a/CompanyEmployee.Api/Program.cs b/CompanyEmployee.Api/Program.cs new file mode 100644 index 00000000..88efd005 --- /dev/null +++ b/CompanyEmployee.Api/Program.cs @@ -0,0 +1,39 @@ +using CompanyEmployee.Api.Services; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); +builder.AddRedisDistributedCache("redis"); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("wasm", policy => + { + policy.AllowAnyOrigin() + .WithMethods("GET") + .WithHeaders("Content-Type"); + }); +}); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.MapDefaultEndpoints(); +app.UseHttpsRedirection(); +app.UseCors("wasm"); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index 9b9aac1a..a679d5cb 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -1,5 +1,19 @@ var builder = DistributedApplication.CreateBuilder(args); -builder.AddProject("companyemployee-api"); +var redis = builder.AddRedis("redis"); -builder.Build().Run(); +var redisCommander = builder.AddContainer("redis-commander", "rediscommander/redis-commander") + .WithEnvironment("REDIS_HOSTS", "local:redis:6379") + .WithReference(redis) + .WaitFor(redis) + .WithEndpoint(port: 8081, targetPort: 8081); + +var api = builder.AddProject("companyemployee-api") + .WithReference(redis) + .WaitFor(redis); + +builder.AddProject("client") + .WithReference(api) + .WaitFor(api); + +builder.Build().Run(); \ No newline at end of file diff --git a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj index 909c1aad..10f90239 100644 --- a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj +++ b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj @@ -11,11 +11,17 @@ - + + + + + + + diff --git a/CompanyEmployee.Domain/CompanyEmployee.Domain.csproj b/CompanyEmployee.Domain/CompanyEmployee.Domain.csproj index 2150e379..fa71b7ae 100644 --- a/CompanyEmployee.Domain/CompanyEmployee.Domain.csproj +++ b/CompanyEmployee.Domain/CompanyEmployee.Domain.csproj @@ -1,7 +1,6 @@  - Exe net8.0 enable enable From e55fc94789419dfdfb0cf92eecfc9db9873ef760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Wed, 25 Feb 2026 00:33:19 +0400 Subject: [PATCH 09/24] =?UTF-8?q?=D0=A7=D1=82=D0=BE-=D1=82=D0=BE=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BB=D1=83=D1=87=D0=B8=D0=BB=D0=BE=D1=81=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Client.Wasm/wwwroot/appsettings.json | 2 +- CompanyEmployee.Api/CompanyEmployee.Api.csproj | 2 ++ CompanyEmployee.Api/Services/EmployeeGenerator.cs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index d1fe7ab3..bc85f89d 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "" + "BaseAddress": "https://localhost:7106/api/employee" } diff --git a/CompanyEmployee.Api/CompanyEmployee.Api.csproj b/CompanyEmployee.Api/CompanyEmployee.Api.csproj index 9caa8568..a64e4e92 100644 --- a/CompanyEmployee.Api/CompanyEmployee.Api.csproj +++ b/CompanyEmployee.Api/CompanyEmployee.Api.csproj @@ -20,4 +20,6 @@ + + diff --git a/CompanyEmployee.Api/Services/EmployeeGenerator.cs b/CompanyEmployee.Api/Services/EmployeeGenerator.cs index ef7b6c33..14794eb0 100644 --- a/CompanyEmployee.Api/Services/EmployeeGenerator.cs +++ b/CompanyEmployee.Api/Services/EmployeeGenerator.cs @@ -24,7 +24,7 @@ public Employee Generate(int? seed = null) if (seed.HasValue) Randomizer.Seed = new Random(seed.Value); - var faker = new Faker(); + var faker = new Faker("ru"); var gender = faker.PickRandom(); var firstName = faker.Name.FirstName(gender); From adec24750cb4ce8faa1594b41d51ef67a2d3fad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Wed, 25 Feb 2026 00:55:19 +0400 Subject: [PATCH 10/24] Delete README.md --- README.md | 128 ------------------------------------------------------ 1 file changed, 128 deletions(-) delete mode 100644 README.md diff --git a/README.md b/README.md deleted file mode 100644 index dcaa5eb7..00000000 --- a/README.md +++ /dev/null @@ -1,128 +0,0 @@ -# Современные технологии разработки программного обеспечения -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1an43o-iqlq4V_kDtkr_y7DC221hY9qdhGPrpII27sH8/edit?usp=sharing) - -## Задание -### Цель -Реализация проекта микросервисного бекенда. - -### Задачи -* Реализация межсервисной коммуникации, -* Изучение работы с брокерами сообщений, -* Изучение архитектурных паттернов, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Интеграционное тестирование. - -### Лабораторные работы -
-1. «Кэширование» - Реализация сервиса генерации контрактов, кэширование его ответов -
- -В рамках первой лабораторной работы необходимо: -* Реализовать сервис генерации контрактов на основе Bogus, -* Реализовать кеширование при помощи IDistributedCache и Redis, -* Реализовать структурное логирование сервиса генерации, -* Настроить оркестрацию Aspire. - -
-
-2. «Балансировка нагрузки» - Реализация апи гейтвея, настройка его работы -
- -В рамках второй лабораторной работы необходимо: -* Настроить оркестрацию на запуск нескольких реплик сервиса генерации, -* Реализовать апи гейтвей на основе Ocelot, -* Имплементировать алгоритм балансировки нагрузки согласно варианту. - -
-
-
-3. «Интеграционное тестирование» - Реализация файлового сервиса и объектного хранилища, интеграционное тестирование бекенда -
- -В рамках третьей лабораторной работы необходимо: -* Добавить в оркестрацию объектное хранилище, -* Реализовать файловый сервис, сериализующий сгенерированные данные в файлы и сохраняющий их в объектном хранилище, -* Реализовать отправку генерируемых данных в файловый сервис посредством брокера, -* Реализовать интеграционные тесты, проверяющие корректность работы всех сервисов бекенда вместе. - -
-
-
-4. (Опционально) «Переход на облачную инфраструктуру» - Перенос бекенда в Yandex Cloud -
- -В рамках четвертой лабораторной работы необходимо перенестиервисы на облако все ранее разработанные сервисы: -* Клиент - в хостинг через отдельный бакет Object Storage, -* Сервис генерации - в Cloud Function, -* Апи гейтвей - в Serverless Integration как API Gateway, -* Брокер сообщений - в Message Queue, -* Файловый сервис - в Cloud Function, -* Объектное хранилище - в отдельный бакет Object Storage, - -
-
- -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). -* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview). -* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus). -* Реализация тестов с использованием [xUnit](https://xunit.net/?tabs=cs). -* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. - -**Факультативно**: -* Перенос бекенда на облачную инфраструктуру Yandex Cloud - -Внимательно прочитайте [дискуссии](https://github.com/itsecd/cloud-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. - -По итогу работы в семестре должна получиться следующая информационная система: -
-C4 диаграмма -Современные_технологии_разработки_ПО_drawio -
- -## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. - -[Список вариантов](https://docs.google.com/document/d/1WGmLYwffTTaAj4TgFCk5bUyW3XKbFMiBm-DHZrfFWr4/edit?usp=sharing) -[Список предметных областей и алгоритмов балансировки](https://docs.google.com/document/d/1PLn2lKe4swIdJDZhwBYzxqFSu0AbY2MFY1SUPkIKOM4/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). - -Общая схема: -1. Сделать форк данного репозитория -2. Выполнить задание -3. Сделать PR в данный репозиторий -4. Исправить замечания после code review -5. Получить approve - -## Критерии оценивания - -Конкурентный принцип. -Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: -1. Скорость разработки -2. Качество разработки -3. Полнота выполнения задания - -Быстрее делаете PR - у вас преимущество. -Быстрее получаете Approve - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. -Не укладываетесь в дедлайн - получаете минимально возможный балл. - -### Шкала оценивания - -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл - -## Вопросы и обратная связь по курсу - -Чтобы задать вопрос по лабораторной, воспользуйтесь [соответствующим разделом дискуссий](https://github.com/itsecd/cloud-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/cloud-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/cloud-development/discussions/categories/ideas). - From 7a0df48803bfb75e612b669d2747b055979b7a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Thu, 26 Feb 2026 22:42:03 +0400 Subject: [PATCH 11/24] =?UTF-8?q?=D0=9D=D0=B0=D0=B2=D0=BE=D1=80=D0=BE?= =?UTF-8?q?=D1=82=D0=B8=D0=BB=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сделал исправления Пожалуйста, не ругайтесь на C#-инвалида (на меня) --- .../CompanyEmployee.Api.csproj | 4 +- CompanyEmployee.Api/CompanyEmployee.Api.http | 6 - .../Controllers/EmployeeController.cs | 51 ++++--- CompanyEmployee.Api/Program.cs | 4 +- .../Services/EmployeeGenerator.cs | 127 +++++++++--------- .../Services/EmployeeService.cs | 58 +++----- CompanyEmployee.Api/Services/ICacheService.cs | 22 +++ .../Services/IEmployeeGenerator.cs | 9 +- .../Services/IEmployeeService.cs | 10 +- .../Services/RedisCacheService.cs | 59 ++++++++ CompanyEmployee.AppHost/AppHost.cs | 15 +-- .../CompanyEmployee.AppHost.csproj | 11 +- CompanyEmployee.Domain/Entity/Employee.cs | 9 -- .../CompanyEmployee.ServiceDefaults.csproj | 4 +- CompanyEmployee.ServiceDefaults/Extensions.cs | 4 +- 15 files changed, 231 insertions(+), 162 deletions(-) delete mode 100644 CompanyEmployee.Api/CompanyEmployee.Api.http create mode 100644 CompanyEmployee.Api/Services/ICacheService.cs create mode 100644 CompanyEmployee.Api/Services/RedisCacheService.cs diff --git a/CompanyEmployee.Api/CompanyEmployee.Api.csproj b/CompanyEmployee.Api/CompanyEmployee.Api.csproj index a64e4e92..9e70639e 100644 --- a/CompanyEmployee.Api/CompanyEmployee.Api.csproj +++ b/CompanyEmployee.Api/CompanyEmployee.Api.csproj @@ -7,10 +7,8 @@ - + - - diff --git a/CompanyEmployee.Api/CompanyEmployee.Api.http b/CompanyEmployee.Api/CompanyEmployee.Api.http deleted file mode 100644 index 4a6b1c11..00000000 --- a/CompanyEmployee.Api/CompanyEmployee.Api.http +++ /dev/null @@ -1,6 +0,0 @@ -@CompanyEmployee.Api_HostAddress = http://localhost:5121 - -GET {{CompanyEmployee.Api_HostAddress}}/weatherforecast/ -Accept: application/json - -### diff --git a/CompanyEmployee.Api/Controllers/EmployeeController.cs b/CompanyEmployee.Api/Controllers/EmployeeController.cs index b01ef5f5..365d9101 100644 --- a/CompanyEmployee.Api/Controllers/EmployeeController.cs +++ b/CompanyEmployee.Api/Controllers/EmployeeController.cs @@ -7,28 +7,49 @@ namespace CompanyEmployee.Api.Controllers; /// /// Контроллер для работы с сотрудниками. /// +/// Сервис для получения сотрудников с кэшированием. +/// Логгер для записи информации о запросах. [ApiController] [Route("api/[controller]")] -public class EmployeeController : ControllerBase +public class EmployeeController( + IEmployeeService employeeService, + ILogger logger) : ControllerBase { - private readonly IEmployeeService _employeeService; - private readonly ILogger _logger; - - public EmployeeController(IEmployeeService employeeService, ILogger logger) - { - _employeeService = employeeService; - _logger = logger; - } - /// - /// Получить сгенерированного сотрудника. + /// Получить сотрудника по идентификатору. /// + /// Идентификатор сотрудника. + /// Токен отмены операции. + /// Объект сотрудника. [HttpGet] [ProducesResponseType(typeof(Employee), StatusCodes.Status200OK)] - public async Task> GetEmployee(int? seed, CancellationToken cancellationToken) + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetEmployee(int id, CancellationToken cancellationToken) { - _logger.LogInformation("Запрос на получение сотрудника с seed: {Seed}", seed); - var employee = await _employeeService.GetEmployeeAsync(seed, cancellationToken); - return Ok(employee); + try + { + logger.LogInformation("Запрос на получение сотрудника с id: {Id}", id); + + if (id <= 0) + { + return BadRequest("ID должен быть положительным числом"); + } + + var employee = await employeeService.GetEmployeeAsync(id, cancellationToken); + + if (employee == null) + { + return NotFound($"Сотрудник с ID {id} не найден"); + } + + return Ok(employee); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка при получении сотрудника с id: {Id}", id); + return StatusCode(500, "Внутренняя ошибка сервера"); + } } } \ No newline at end of file diff --git a/CompanyEmployee.Api/Program.cs b/CompanyEmployee.Api/Program.cs index 88efd005..9ed09847 100644 --- a/CompanyEmployee.Api/Program.cs +++ b/CompanyEmployee.Api/Program.cs @@ -1,10 +1,10 @@ using CompanyEmployee.Api.Services; +using CompanyEmployee.ServiceDefaults; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); builder.AddRedisDistributedCache("redis"); - builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); @@ -20,6 +20,7 @@ }); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddScoped(); var app = builder.Build(); @@ -35,5 +36,4 @@ app.UseCors("wasm"); app.UseAuthorization(); app.MapControllers(); - app.Run(); \ No newline at end of file diff --git a/CompanyEmployee.Api/Services/EmployeeGenerator.cs b/CompanyEmployee.Api/Services/EmployeeGenerator.cs index 14794eb0..100cc7e2 100644 --- a/CompanyEmployee.Api/Services/EmployeeGenerator.cs +++ b/CompanyEmployee.Api/Services/EmployeeGenerator.cs @@ -1,83 +1,78 @@ using Bogus; using Bogus.DataSets; using CompanyEmployee.Domain.Entity; -using System.Xml.Linq; namespace CompanyEmployee.Api.Services; -public class EmployeeGenerator : IEmployeeGenerator +/// +/// Генератор сотрудников. +/// +/// Логгер. +public class EmployeeGenerator(ILogger logger) : IEmployeeGenerator { - private readonly ILogger _logger; private readonly string[] _professions = { "Developer", "Manager", "Analyst", "Designer", "QA" }; private readonly string[] _suffixes = { "Junior", "Middle", "Senior" }; - public EmployeeGenerator(ILogger logger) + /// + public Employee Generate(int id) { - _logger = logger; - } - - /// - /// Генератор сотрудника - /// - public Employee Generate(int? seed = null) - { - if (seed.HasValue) - Randomizer.Seed = new Random(seed.Value); - + Randomizer.Seed = new Random(id); var faker = new Faker("ru"); + var employee = new Faker("ru") + .RuleFor(e => e.Id, id) + .RuleFor(e => e.FullName, f => + { + var gender = f.PickRandom(); + var firstName = f.Name.FirstName(gender); + var lastName = f.Name.LastName(gender); + var fatherName = f.Name.FirstName(Name.Gender.Male); + var patronymic = gender == Name.Gender.Male + ? fatherName.EndsWith("й") || fatherName.EndsWith("ь") + ? fatherName[..^1] + "евич" + : fatherName + "ович" + : fatherName.EndsWith("й") || fatherName.EndsWith("ь") + ? fatherName[..^1] + "евна" + : fatherName + "овна"; - var gender = faker.PickRandom(); - var firstName = faker.Name.FirstName(gender); - var lastName = faker.Name.LastName(gender); - var patronymicBase = faker.Name.FirstName(Name.Gender.Male); - var patronymic = gender == Name.Gender.Male - ? patronymicBase + "ович" - : patronymicBase + "овна"; - var fullName = $"{lastName} {firstName} {patronymic}"; - - var profession = faker.PickRandom(_professions); - var suffix = faker.PickRandom(_suffixes); - var position = $"{profession} {suffix}".Trim(); - - var department = faker.Commerce.Department(); - - var hireDate = DateOnly.FromDateTime(faker.Date.Past(10).ToUniversalTime()); - - var salary = suffix switch - { - "Junior" => faker.Random.Decimal(30000, 60000), - "Middle" => faker.Random.Decimal(60000, 100000), - "Senior" => faker.Random.Decimal(100000, 180000), - _ => faker.Random.Decimal(40000, 80000) - }; - salary = Math.Round(salary, 2); - - var email = faker.Internet.Email(firstName, lastName); - var phone = faker.Phone.PhoneNumber("+7(###)###-##-##"); - var isTerminated = faker.Random.Bool(0.1f); - - DateOnly? terminationDate = null; - if (isTerminated) - { - var termDate = faker.Date.Between(hireDate.ToDateTime(TimeOnly.MinValue), DateTime.Now); - terminationDate = DateOnly.FromDateTime(termDate); - } - - var employee = new Employee - { - Id = faker.Random.Int(1, 100000), - FullName = fullName, - Position = position, - Department = department, - HireDate = hireDate, - Salary = salary, - Email = email, - Phone = phone, - IsTerminated = isTerminated, - TerminationDate = terminationDate - }; + return $"{lastName} {firstName} {patronymic}"; + }) + .RuleFor(e => e.Position, f => + { + var profession = f.PickRandom(_professions); + var suffix = f.PickRandom(_suffixes); + return $"{profession} {suffix}"; + }) + .RuleFor(e => e.Department, f => f.Commerce.Department()) + .RuleFor(e => e.HireDate, f => + DateOnly.FromDateTime(f.Date.Past(10).ToUniversalTime())) + .RuleFor(e => e.Salary, f => + { + var suffix = f.PickRandom(_suffixes); + var salary = suffix switch + { + "Junior" => f.Random.Decimal(30000, 60000), + "Middle" => f.Random.Decimal(60000, 100000), + "Senior" => f.Random.Decimal(100000, 180000), + _ => f.Random.Decimal(40000, 80000) + }; + return Math.Round(salary, 2); + }) + .RuleFor(e => e.Email, (f, e) => + { + var nameParts = e.FullName.Split(' '); + return f.Internet.Email(nameParts[1], nameParts[0], "company.ru"); + }) + .RuleFor(e => e.Phone, f => f.Phone.PhoneNumber("+7(###)###-##-##")) + .RuleFor(e => e.IsTerminated, f => f.Random.Bool(0.1f)) + .RuleFor(e => e.TerminationDate, (f, e) => + e.IsTerminated + ? DateOnly.FromDateTime(f.Date.Between( + e.HireDate.ToDateTime(TimeOnly.MinValue), + DateTime.Now)) + : null) + .Generate(); - _logger.LogInformation("Сгенерирован новый сотрудник: {@Employee}", employee); + logger.LogInformation("Сгенерирован сотрудник ID {Id}: {FullName}", employee.Id, employee.FullName); return employee; } } \ No newline at end of file diff --git a/CompanyEmployee.Api/Services/EmployeeService.cs b/CompanyEmployee.Api/Services/EmployeeService.cs index d6513074..11b29566 100644 --- a/CompanyEmployee.Api/Services/EmployeeService.cs +++ b/CompanyEmployee.Api/Services/EmployeeService.cs @@ -1,54 +1,40 @@ using CompanyEmployee.Domain.Entity; using Microsoft.Extensions.Caching.Distributed; -using System.Text.Json; namespace CompanyEmployee.Api.Services; /// -/// Реализация сервиса сотрудников с кэшированием в Redis через IDistributedCache. +/// Бизнес-логика работы с сотрудниками. /// -public class EmployeeService : IEmployeeService +/// Генератор сотрудников. +/// Сервис кэширования. +/// Логгер. +public class EmployeeService( + IEmployeeGenerator generator, + ICacheService cache, + ILogger logger) : IEmployeeService { - private readonly IEmployeeGenerator _generator; - private readonly IDistributedCache _cache; - private readonly ILogger _logger; - private readonly DistributedCacheEntryOptions _cacheOptions; - - public EmployeeService(IEmployeeGenerator generator, IDistributedCache cache, ILogger logger) + private readonly DistributedCacheEntryOptions _cacheOptions = new() { - _generator = generator; - _cache = cache; - _logger = logger; - _cacheOptions = new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) - }; - } + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) + }; - /// - /// Получает сотрудника. - /// - public async Task GetEmployeeAsync(int? seed, CancellationToken cancellationToken = default) + /// + public async Task GetEmployeeAsync(int id, CancellationToken cancellationToken = default) { - var cacheKey = seed.HasValue ? $"employee:seed:{seed}" : $"employee:random:{Guid.NewGuid()}"; - - _logger.LogDebug("Попытка получения сотрудника из кэша по ключу {CacheKey}", cacheKey); - - var cachedJson = await _cache.GetStringAsync(cacheKey, cancellationToken); - if (cachedJson != null) + var cacheKey = $"employee:{id}"; + var employee = await cache.GetAsync(cacheKey, cancellationToken); + if (employee != null) { - _logger.LogInformation("Сотрудник найден в кэше по ключу {CacheKey}", cacheKey); - var employee = JsonSerializer.Deserialize(cachedJson); - return employee!; + logger.LogInformation("Сотрудник с ID {Id} найден в кэше", id); + return employee; } - _logger.LogInformation("Сотрудник не найден в кэше, генерация нового. Seed: {Seed}", seed); - var newEmployee = _generator.Generate(seed); + logger.LogInformation("Сотрудник с ID {Id} не найден в кэше, генерация нового", id); + employee = generator.Generate(id); - var serialized = JsonSerializer.Serialize(newEmployee); - await _cache.SetStringAsync(cacheKey, serialized, _cacheOptions, cancellationToken); - _logger.LogDebug("Сгенерированный сотрудник сохранён в кэш по ключу {CacheKey}", cacheKey); + await cache.SetAsync(cacheKey, employee, _cacheOptions, cancellationToken); - return newEmployee; + return employee; } } \ No newline at end of file diff --git a/CompanyEmployee.Api/Services/ICacheService.cs b/CompanyEmployee.Api/Services/ICacheService.cs new file mode 100644 index 00000000..523bf771 --- /dev/null +++ b/CompanyEmployee.Api/Services/ICacheService.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.Caching.Distributed; + +namespace CompanyEmployee.Api.Services; + +/// +/// Сервис для работы с распределённым кэшем. +/// +public interface ICacheService +{ + /// Получает данные из кэша по ключу. + /// Ключ кэша. + /// Токен отмены. + /// Данные из кэша или default. + public Task GetAsync(string key, CancellationToken cancellationToken = default); + + /// Сохраняет данные в кэш. + /// Ключ кэша. + /// Данные для сохранения. + /// Опции кэширования. + /// Токен отмены. + public Task SetAsync(string key, T value, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/CompanyEmployee.Api/Services/IEmployeeGenerator.cs b/CompanyEmployee.Api/Services/IEmployeeGenerator.cs index b0b4e31d..812b2340 100644 --- a/CompanyEmployee.Api/Services/IEmployeeGenerator.cs +++ b/CompanyEmployee.Api/Services/IEmployeeGenerator.cs @@ -2,10 +2,15 @@ namespace CompanyEmployee.Api.Services; +/// +/// Генератор данных сотрудников. +/// public interface IEmployeeGenerator { /// - /// Генерирует нового сотрудника. + /// Генерирует сотрудника по идентификатору. /// - public Employee Generate(int? seed = null); + /// Идентификатор. + /// Сгенерированный сотрудник. + public Employee Generate(int id); } \ No newline at end of file diff --git a/CompanyEmployee.Api/Services/IEmployeeService.cs b/CompanyEmployee.Api/Services/IEmployeeService.cs index 9bc30923..000463b8 100644 --- a/CompanyEmployee.Api/Services/IEmployeeService.cs +++ b/CompanyEmployee.Api/Services/IEmployeeService.cs @@ -2,10 +2,16 @@ namespace CompanyEmployee.Api.Services; +/// +/// Сервис для работы с сотрудниками. +/// public interface IEmployeeService { /// - /// Получает сотрудника. + /// Получает сотрудника по идентификатору с использованием кэширования. /// - public Task GetEmployeeAsync(int? seed, CancellationToken cancellationToken = default); + /// Идентификатор сотрудника. + /// Токен отмены. + /// Сотрудник или null. + public Task GetEmployeeAsync(int id, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/CompanyEmployee.Api/Services/RedisCacheService.cs b/CompanyEmployee.Api/Services/RedisCacheService.cs new file mode 100644 index 00000000..e9c68aab --- /dev/null +++ b/CompanyEmployee.Api/Services/RedisCacheService.cs @@ -0,0 +1,59 @@ +using System.Text.Json; +using Microsoft.Extensions.Caching.Distributed; + +namespace CompanyEmployee.Api.Services; + +/// +/// Реализация кэширования в Redis. +/// +/// Redis Distributed Cache. +/// Логгер. +public class RedisCacheService( + IDistributedCache cache, + ILogger logger) : ICacheService +{ + /// + public async Task GetAsync(string key, CancellationToken cancellationToken = default) + { + try + { + logger.LogDebug("Получение из кэша по ключу {Key}", key); + var cachedJson = await cache.GetStringAsync(key, cancellationToken); + + if (cachedJson == null) + { + logger.LogDebug("Данные по ключу {Key} не найдены", key); + return default; + } + + return JsonSerializer.Deserialize(cachedJson); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка при получении данных из кэша по ключу {Key}", key); + return default; + } + } + + /// + public async Task SetAsync(string key, T value, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default) + { + try + { + logger.LogDebug("Сохранение в кэш по ключу {Key}", key); + var serialized = JsonSerializer.Serialize(value); + + await cache.SetStringAsync( + key, + serialized, + options ?? new DistributedCacheEntryOptions(), + cancellationToken); + + logger.LogDebug("Данные сохранены в кэш по ключу {Key}", key); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Не удалось сохранить данные в кэш по ключу {Key}", key); + } + } +} \ No newline at end of file diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index a679d5cb..6e02e8d8 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -1,19 +1,14 @@ var builder = DistributedApplication.CreateBuilder(args); -var redis = builder.AddRedis("redis"); - -var redisCommander = builder.AddContainer("redis-commander", "rediscommander/redis-commander") - .WithEnvironment("REDIS_HOSTS", "local:redis:6379") - .WithReference(redis) - .WaitFor(redis) - .WithEndpoint(port: 8081, targetPort: 8081); +var redis = builder.AddRedis("redis") + .WithRedisCommander(); var api = builder.AddProject("companyemployee-api") .WithReference(redis) - .WaitFor(redis); + .WaitFor(redis); builder.AddProject("client") - .WithReference(api) - .WaitFor(api); + .WithReference(api) + .WaitFor(api); builder.Build().Run(); \ No newline at end of file diff --git a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj index 10f90239..1a392b48 100644 --- a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj +++ b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj @@ -1,4 +1,4 @@ - + @@ -11,17 +11,14 @@ - - - - + + + - - diff --git a/CompanyEmployee.Domain/Entity/Employee.cs b/CompanyEmployee.Domain/Entity/Employee.cs index 86638159..8bdc4aea 100644 --- a/CompanyEmployee.Domain/Entity/Employee.cs +++ b/CompanyEmployee.Domain/Entity/Employee.cs @@ -7,55 +7,46 @@ public class Employee /// public int Id { get; set; } - /// /// ФИО /// public string FullName { get; set; } = string.Empty; - /// /// Должность /// public string Position { get; set; } = string.Empty; - /// /// Отдел /// public string Department { get; set; } = string.Empty; - /// /// Дата приема /// public DateOnly HireDate { get; set; } - /// /// Зарплата /// public decimal Salary { get; set; } - /// /// Электронная почта /// public string Email { get; set; } = string.Empty; - /// /// Телефон /// public string Phone { get; set; } = string.Empty; - /// /// Индикатор увольнения /// public bool IsTerminated { get; set; } - /// /// Дата увольнения /// diff --git a/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj b/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj index d808731c..c8234c28 100644 --- a/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj +++ b/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/CompanyEmployee.ServiceDefaults/Extensions.cs b/CompanyEmployee.ServiceDefaults/Extensions.cs index 112c1281..e23f6c60 100644 --- a/CompanyEmployee.ServiceDefaults/Extensions.cs +++ b/CompanyEmployee.ServiceDefaults/Extensions.cs @@ -2,13 +2,13 @@ using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.ServiceDiscovery; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Trace; -namespace Microsoft.Extensions.Hosting; +namespace CompanyEmployee.ServiceDefaults; // Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. // This project should be referenced by each service project in your solution. From 839842f08005c78fb2ab70f1b0e76f479634d691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Sat, 28 Feb 2026 10:01:42 +0400 Subject: [PATCH 12/24] Update Employee.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавил недостающее summary --- CompanyEmployee.Domain/Entity/Employee.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CompanyEmployee.Domain/Entity/Employee.cs b/CompanyEmployee.Domain/Entity/Employee.cs index 8bdc4aea..88b7935e 100644 --- a/CompanyEmployee.Domain/Entity/Employee.cs +++ b/CompanyEmployee.Domain/Entity/Employee.cs @@ -1,5 +1,8 @@ namespace CompanyEmployee.Domain.Entity; +/// +/// Представляет сотрудника компании со всеми характеристиками. +/// public class Employee { /// From 3640a4b907874eb5304482baa7d0c2e9ff029b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Tue, 10 Mar 2026 21:15:07 +0400 Subject: [PATCH 13/24] =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=B0=D0=BB=D0=BE=202?= =?UTF-8?q?=20=D0=BB=D0=B0=D0=B1=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CloudDevelopment.sln | 6 +++ CompanyEmployee.AppHost/AppHost.cs | 2 + .../CompanyEmployee.AppHost.csproj | 1 + .../CompanyEmployee.Gateway.csproj | 17 +++++++ CompanyEmployee.Gateway/Program.cs | 29 ++++++++++++ .../Properties/launchSettings.json | 41 +++++++++++++++++ CompanyEmployee.Gateway/WeatherForecast.cs | 12 +++++ .../appsettings.Development.json | 8 ++++ CompanyEmployee.Gateway/appsettings.json | 9 ++++ CompanyEmployee.Gateway/ocelot.json | 44 +++++++++++++++++++ 10 files changed, 169 insertions(+) create mode 100644 CompanyEmployee.Gateway/CompanyEmployee.Gateway.csproj create mode 100644 CompanyEmployee.Gateway/Program.cs create mode 100644 CompanyEmployee.Gateway/Properties/launchSettings.json create mode 100644 CompanyEmployee.Gateway/WeatherForecast.cs create mode 100644 CompanyEmployee.Gateway/appsettings.Development.json create mode 100644 CompanyEmployee.Gateway/appsettings.json create mode 100644 CompanyEmployee.Gateway/ocelot.json diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index 3a39d831..edbb81dc 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.Domain", "C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.Api", "CompanyEmployee.Api\CompanyEmployee.Api.csproj", "{EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.Gateway", "CompanyEmployee.Gateway\CompanyEmployee.Gateway.csproj", "{73C5B926-EAC3-32E1-1AD5-911888B0E715}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Debug|Any CPU.Build.0 = Debug|Any CPU {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Release|Any CPU.ActiveCfg = Release|Any CPU {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Release|Any CPU.Build.0 = Release|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Release|Any CPU.ActiveCfg = Release|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index 6e02e8d8..8af58d08 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -11,4 +11,6 @@ .WithReference(api) .WaitFor(api); +builder.AddProject("companyemployee-gateway"); + builder.Build().Run(); \ No newline at end of file diff --git a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj index 1a392b48..abad211b 100644 --- a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj +++ b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj @@ -19,6 +19,7 @@ + diff --git a/CompanyEmployee.Gateway/CompanyEmployee.Gateway.csproj b/CompanyEmployee.Gateway/CompanyEmployee.Gateway.csproj new file mode 100644 index 00000000..012ebd69 --- /dev/null +++ b/CompanyEmployee.Gateway/CompanyEmployee.Gateway.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + diff --git a/CompanyEmployee.Gateway/Program.cs b/CompanyEmployee.Gateway/Program.cs new file mode 100644 index 00000000..edda8a7f --- /dev/null +++ b/CompanyEmployee.Gateway/Program.cs @@ -0,0 +1,29 @@ +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +app.MapDefaultEndpoints(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/CompanyEmployee.Gateway/Properties/launchSettings.json b/CompanyEmployee.Gateway/Properties/launchSettings.json new file mode 100644 index 00000000..eb75214f --- /dev/null +++ b/CompanyEmployee.Gateway/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28509", + "sslPort": 44389 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5144", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7073;http://localhost:5144", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/CompanyEmployee.Gateway/WeatherForecast.cs b/CompanyEmployee.Gateway/WeatherForecast.cs new file mode 100644 index 00000000..74bedbdf --- /dev/null +++ b/CompanyEmployee.Gateway/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace CompanyEmployee.Gateway; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} diff --git a/CompanyEmployee.Gateway/appsettings.Development.json b/CompanyEmployee.Gateway/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/CompanyEmployee.Gateway/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CompanyEmployee.Gateway/appsettings.json b/CompanyEmployee.Gateway/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/CompanyEmployee.Gateway/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/CompanyEmployee.Gateway/ocelot.json b/CompanyEmployee.Gateway/ocelot.json new file mode 100644 index 00000000..a61ddf56 --- /dev/null +++ b/CompanyEmployee.Gateway/ocelot.json @@ -0,0 +1,44 @@ +{ + "Routes": [ + { + "DownstreamPathTemplate": "/api/employee", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5001 + }, + { + "Host": "localhost", + "Port": 5002 + }, + { + "Host": "localhost", + "Port": 5003 + }, + { + "Host": "localhost", + "Port": 5004 + }, + { + "Host": "localhost", + "Port": 5005 + } + ], + "UpstreamPathTemplate": "/api/employee", + "UpstreamHttpMethod": [ "GET" ], + "LoadBalancerOptions": { + "Type": "WeightedRandom", + "Key": "employee-service" + }, + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 30000, + "TimeoutValue": 5000 + } + } + ], + "GlobalConfiguration": { + "BaseUrl": "https://localhost:7000" + } +} \ No newline at end of file From 1a05646f6a2f337a191a959b3fd1c1841d5558a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Tue, 10 Mar 2026 22:10:51 +0400 Subject: [PATCH 14/24] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Надеюсь, я делаю что-то похожее на правду --- CompanyEmployee.AppHost/AppHost.cs | 30 +++++-- .../CompanyEmployee.Gateway.csproj | 6 ++ .../WeightedRandomLoadBalancer.cs | 82 +++++++++++++++++++ .../WeightedRandomLoadBalancerFactory.cs | 47 +++++++++++ CompanyEmployee.Gateway/Program.cs | 42 ++++++---- CompanyEmployee.Gateway/WeatherForecast.cs | 12 --- CompanyEmployee.Gateway/ocelot.json | 12 +-- 7 files changed, 189 insertions(+), 42 deletions(-) create mode 100644 CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancer.cs create mode 100644 CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancerFactory.cs delete mode 100644 CompanyEmployee.Gateway/WeatherForecast.cs diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index 8af58d08..5ee7583c 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -3,14 +3,30 @@ var redis = builder.AddRedis("redis") .WithRedisCommander(); -var api = builder.AddProject("companyemployee-api") - .WithReference(redis) - .WaitFor(redis); +var gateway = builder.AddProject("gateway") + .WithEndpoint("https", e => e.Port = 7000, createIfNotExists: true) + .WithExternalHttpEndpoints(); -builder.AddProject("client") - .WithReference(api) - .WaitFor(api); +const int startApiPort = 6001; +const int replicaCount = 5; -builder.AddProject("companyemployee-gateway"); +for (var i = 0; i < replicaCount; i++) +{ + var port = startApiPort + i; + var url = "https://localhost:" + port.ToString(); + + var api = builder.AddProject($"api-{i + 1}") + .WithReference(redis) + .WithEndpoint("https", e => e.Port = port, createIfNotExists: true) + .WithEnvironment("ASPNETCORE_URLS", url) + .WaitFor(redis); + + gateway.WaitFor(api); +} + +var client = builder.AddProject("client") + .WithReference(gateway) + .WithEnvironment("API_URL", "https://localhost:7000") + .WaitFor(gateway); builder.Build().Run(); \ No newline at end of file diff --git a/CompanyEmployee.Gateway/CompanyEmployee.Gateway.csproj b/CompanyEmployee.Gateway/CompanyEmployee.Gateway.csproj index 012ebd69..31d4bc8c 100644 --- a/CompanyEmployee.Gateway/CompanyEmployee.Gateway.csproj +++ b/CompanyEmployee.Gateway/CompanyEmployee.Gateway.csproj @@ -7,6 +7,8 @@ + + @@ -14,4 +16,8 @@ + + + + diff --git a/CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancer.cs b/CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancer.cs new file mode 100644 index 00000000..122f831d --- /dev/null +++ b/CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancer.cs @@ -0,0 +1,82 @@ +using Ocelot.Errors; +using Ocelot.LoadBalancer.Interfaces; +using Ocelot.Responses; +using Ocelot.Values; + +namespace CompanyEmployee.Gateway.LoadBalancers; + +/// +/// Балансировщик с взвешенным случайным распределением запросов. +/// +public class WeightedRandomLoadBalancer : ILoadBalancer +{ + private readonly List _services; + private readonly Dictionary _weights; + private readonly Random _random = new(); + private readonly object _lock = new(); + + /// + /// Тип балансировщика. + /// + public string Type => nameof(WeightedRandomLoadBalancer); + + /// + /// Инициализирует новый экземпляр балансировщика с весами. + /// + /// Список сервисов. + /// Словарь весов для каждого сервиса. + public WeightedRandomLoadBalancer(List services, Dictionary weights) + { + _services = services; + _weights = weights; + } + + /// + public Task> LeaseAsync(HttpContext httpContext) + { + lock (_lock) + { + var weightedList = new List(); + + foreach (var service in _services) + { + var key = $"{service.HostAndPort.DownstreamHost}:{service.HostAndPort.DownstreamPort}"; + var weight = _weights.GetValueOrDefault(key, 1); + + for (var i = 0; i < weight; i++) + { + weightedList.Add(service); + } + } + + if (weightedList.Count == 0) + { + return Task.FromResult>( + new ErrorResponse(new List + { + new UnableToFindServiceError() + })); + } + + var selected = weightedList[_random.Next(weightedList.Count)]; + return Task.FromResult>( + new OkResponse(selected.HostAndPort)); + } + } + + /// + public void Release(ServiceHostAndPort hostAndPort) + { + } +} + +/// +/// Ошибка, возникающая когда сервис не найден. +/// +public class UnableToFindServiceError : Error +{ + public UnableToFindServiceError() + : base("Нет доступных сервисов для обработки запроса", OcelotErrorCode.UnableToFindDownstreamRouteError, 404) + { + } +} \ No newline at end of file diff --git a/CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancerFactory.cs b/CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancerFactory.cs new file mode 100644 index 00000000..b4bd10a3 --- /dev/null +++ b/CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancerFactory.cs @@ -0,0 +1,47 @@ +using Ocelot.Configuration; +using Ocelot.LoadBalancer.Interfaces; +using Ocelot.Responses; +using Ocelot.Values; + +namespace CompanyEmployee.Gateway.LoadBalancers; + +/// +/// Фабрика для создания взвешенного случайного балансировщика. +/// +public class WeightedRandomLoadBalancerFactory : ILoadBalancerFactory +{ + private readonly Dictionary _defaultWeights = new() + { + { "localhost:6001", 5 }, + { "localhost:6002", 4 }, + { "localhost:6003", 3 }, + { "localhost:6004", 2 }, + { "localhost:6005", 1 } + }; + + /// + public Response Get(DownstreamRoute route, ServiceProviderConfiguration config) + { + var services = new List(); + + foreach (var address in route.DownstreamAddresses) + { + var hostAndPort = new ServiceHostAndPort( + address.Host, + address.Port, + route.DownstreamScheme); + + var service = new Service( + $"{address.Host}:{address.Port}", + hostAndPort, + string.Empty, + string.Empty, + Enumerable.Empty()); + + services.Add(service); + } + + var loadBalancer = new WeightedRandomLoadBalancer(services, _defaultWeights); + return new OkResponse(loadBalancer); + } +} \ No newline at end of file diff --git a/CompanyEmployee.Gateway/Program.cs b/CompanyEmployee.Gateway/Program.cs index edda8a7f..42408388 100644 --- a/CompanyEmployee.Gateway/Program.cs +++ b/CompanyEmployee.Gateway/Program.cs @@ -1,29 +1,37 @@ +using CompanyEmployee.Gateway.LoadBalancers; +using CompanyEmployee.ServiceDefaults; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using Ocelot.Provider.Polly; +using Ocelot.LoadBalancer.Interfaces; + var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -// Add services to the container. +builder.Configuration.AddJsonFile("ocelot.json", false, true); -builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddCors(options => +{ + options.AddPolicy("wasm", policy => + { + policy.AllowAnyOrigin() + .WithMethods("GET") + .WithHeaders("Content-Type"); + }); +}); -var app = builder.Build(); +builder.Services.AddOcelot() + .AddPolly(); -app.MapDefaultEndpoints(); +builder.Services.AddSingleton(); -// Configure the HTTP request pipeline. -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} +var app = builder.Build(); +app.MapDefaultEndpoints(); +app.UseCors("wasm"); app.UseHttpsRedirection(); -app.UseAuthorization(); - -app.MapControllers(); +await app.UseOcelot(); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/CompanyEmployee.Gateway/WeatherForecast.cs b/CompanyEmployee.Gateway/WeatherForecast.cs deleted file mode 100644 index 74bedbdf..00000000 --- a/CompanyEmployee.Gateway/WeatherForecast.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace CompanyEmployee.Gateway; - -public class WeatherForecast -{ - public DateOnly Date { get; set; } - - public int TemperatureC { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - - public string? Summary { get; set; } -} diff --git a/CompanyEmployee.Gateway/ocelot.json b/CompanyEmployee.Gateway/ocelot.json index a61ddf56..5cacb135 100644 --- a/CompanyEmployee.Gateway/ocelot.json +++ b/CompanyEmployee.Gateway/ocelot.json @@ -3,26 +3,26 @@ { "DownstreamPathTemplate": "/api/employee", "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ + "DownstreamAddresses": [ { "Host": "localhost", - "Port": 5001 + "Port": 6001 }, { "Host": "localhost", - "Port": 5002 + "Port": 6002 }, { "Host": "localhost", - "Port": 5003 + "Port": 6003 }, { "Host": "localhost", - "Port": 5004 + "Port": 6004 }, { "Host": "localhost", - "Port": 5005 + "Port": 6005 } ], "UpstreamPathTemplate": "/api/employee", From fcd95acb4574b7e2c59f89f449839ea046cb152b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Tue, 10 Mar 2026 23:39:05 +0400 Subject: [PATCH 15/24] =?UTF-8?q?=D0=92=D1=80=D0=BE=D0=B4=D0=B5,=20=D1=81?= =?UTF-8?q?=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Скорее всего где-то накосячил, не ругайтесь на меня --- Client.Wasm/Components/StudentCard.razor | 4 ++-- Client.Wasm/wwwroot/appsettings.json | 2 +- CompanyEmployee.Api/Properties/launchSettings.json | 10 +++++----- CompanyEmployee.AppHost/AppHost.cs | 12 +++++------- .../WeightedRandomLoadBalancerFactory.cs | 4 ++-- .../Properties/launchSettings.json | 10 +++++----- CompanyEmployee.Gateway/ocelot.json | 4 ++-- 7 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 419e4ce1..0b487b1b 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -4,8 +4,8 @@ - Номер №1 "Кэширование" - Вариант №43 "Сотрудник компании" + Номер №2 "Балансировка нагрузки" + Вариант №43 "Weighted Random" Выполнена Казаковым Андреем 6513 Ссылка на форк diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index bc85f89d..8e21597f 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "https://localhost:7106/api/employee" + "BaseAddress": "https://localhost:7001/api/employee" } diff --git a/CompanyEmployee.Api/Properties/launchSettings.json b/CompanyEmployee.Api/Properties/launchSettings.json index 2db4f09e..d5306a4e 100644 --- a/CompanyEmployee.Api/Properties/launchSettings.json +++ b/CompanyEmployee.Api/Properties/launchSettings.json @@ -12,9 +12,9 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5121", + "applicationUrl": "http://localhost:6001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -22,9 +22,9 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7106;http://localhost:5121", + "applicationUrl": "https://localhost:6001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -38,4 +38,4 @@ } } } -} +} \ No newline at end of file diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index 5ee7583c..e1c00077 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -1,10 +1,10 @@ var builder = DistributedApplication.CreateBuilder(args); var redis = builder.AddRedis("redis") - .WithRedisCommander(); + .WithRedisCommander(containerName: "redis-commander"); var gateway = builder.AddProject("gateway") - .WithEndpoint("https", e => e.Port = 7000, createIfNotExists: true) + .WithEndpoint("https", e => e.Port = 7001) .WithExternalHttpEndpoints(); const int startApiPort = 6001; @@ -13,12 +13,10 @@ for (var i = 0; i < replicaCount; i++) { var port = startApiPort + i; - var url = "https://localhost:" + port.ToString(); - var api = builder.AddProject($"api-{i + 1}") + .WithEndpoint("https", e => e.Port = port) .WithReference(redis) - .WithEndpoint("https", e => e.Port = port, createIfNotExists: true) - .WithEnvironment("ASPNETCORE_URLS", url) + .WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development") .WaitFor(redis); gateway.WaitFor(api); @@ -26,7 +24,7 @@ var client = builder.AddProject("client") .WithReference(gateway) - .WithEnvironment("API_URL", "https://localhost:7000") + .WithEnvironment("API_URL", "https://localhost:7001") .WaitFor(gateway); builder.Build().Run(); \ No newline at end of file diff --git a/CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancerFactory.cs b/CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancerFactory.cs index b4bd10a3..866f4902 100644 --- a/CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancerFactory.cs +++ b/CompanyEmployee.Gateway/LoadBalancers/WeightedRandomLoadBalancerFactory.cs @@ -26,14 +26,14 @@ public Response Get(DownstreamRoute route, ServiceProviderConfigu foreach (var address in route.DownstreamAddresses) { - var hostAndPort = new ServiceHostAndPort( + var serviceHostAndPort = new ServiceHostAndPort( address.Host, address.Port, route.DownstreamScheme); var service = new Service( $"{address.Host}:{address.Port}", - hostAndPort, + serviceHostAndPort, string.Empty, string.Empty, Enumerable.Empty()); diff --git a/CompanyEmployee.Gateway/Properties/launchSettings.json b/CompanyEmployee.Gateway/Properties/launchSettings.json index eb75214f..3c66d5ce 100644 --- a/CompanyEmployee.Gateway/Properties/launchSettings.json +++ b/CompanyEmployee.Gateway/Properties/launchSettings.json @@ -12,9 +12,9 @@ "http": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5144", + "applicationUrl": "http://localhost:7001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -22,9 +22,9 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7073;http://localhost:5144", + "applicationUrl": "https://localhost:7001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -38,4 +38,4 @@ } } } -} +} \ No newline at end of file diff --git a/CompanyEmployee.Gateway/ocelot.json b/CompanyEmployee.Gateway/ocelot.json index 5cacb135..1bf4faf0 100644 --- a/CompanyEmployee.Gateway/ocelot.json +++ b/CompanyEmployee.Gateway/ocelot.json @@ -3,7 +3,7 @@ { "DownstreamPathTemplate": "/api/employee", "DownstreamScheme": "https", - "DownstreamAddresses": [ + "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 6001 @@ -39,6 +39,6 @@ } ], "GlobalConfiguration": { - "BaseUrl": "https://localhost:7000" + "BaseUrl": "https://localhost:7001" } } \ No newline at end of file From 1a7ce23492e93989d4cbfc1b972c5df55d4fc135 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Thu, 12 Mar 2026 14:47:45 +0400 Subject: [PATCH 16/24] Fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Убрал CORS в API, исправил недочет в AppHost.cs и обновил пакеты до версии 9.5.0 --- CompanyEmployee.Api/CompanyEmployee.Api.csproj | 2 +- CompanyEmployee.Api/Program.cs | 11 ----------- CompanyEmployee.AppHost/AppHost.cs | 1 - .../CompanyEmployee.AppHost.csproj | 6 +++--- .../CompanyEmployee.ServiceDefaults.csproj | 4 ++-- 5 files changed, 6 insertions(+), 18 deletions(-) diff --git a/CompanyEmployee.Api/CompanyEmployee.Api.csproj b/CompanyEmployee.Api/CompanyEmployee.Api.csproj index 9e70639e..f128ff9c 100644 --- a/CompanyEmployee.Api/CompanyEmployee.Api.csproj +++ b/CompanyEmployee.Api/CompanyEmployee.Api.csproj @@ -7,7 +7,7 @@ - + diff --git a/CompanyEmployee.Api/Program.cs b/CompanyEmployee.Api/Program.cs index 9ed09847..99a787de 100644 --- a/CompanyEmployee.Api/Program.cs +++ b/CompanyEmployee.Api/Program.cs @@ -9,16 +9,6 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddCors(options => -{ - options.AddPolicy("wasm", policy => - { - policy.AllowAnyOrigin() - .WithMethods("GET") - .WithHeaders("Content-Type"); - }); -}); - builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); @@ -33,7 +23,6 @@ app.MapDefaultEndpoints(); app.UseHttpsRedirection(); -app.UseCors("wasm"); app.UseAuthorization(); app.MapControllers(); app.Run(); \ No newline at end of file diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index e1c00077..9622058d 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -24,7 +24,6 @@ var client = builder.AddProject("client") .WithReference(gateway) - .WithEnvironment("API_URL", "https://localhost:7001") .WaitFor(gateway); builder.Build().Run(); \ No newline at end of file diff --git a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj index abad211b..60dac8dc 100644 --- a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj +++ b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj @@ -11,9 +11,9 @@ - - - + + + diff --git a/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj b/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj index c8234c28..6ee49167 100644 --- a/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj +++ b/CompanyEmployee.ServiceDefaults/CompanyEmployee.ServiceDefaults.csproj @@ -10,8 +10,8 @@ - - + + From 08662e9bf68b21c2e3b82a298100d6b142a52861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 3 Apr 2026 19:30:46 +0400 Subject: [PATCH 17/24] =?UTF-8?q?=D0=9D=D0=B0=D1=87=D0=B0=D0=BB=D0=BE=203-?= =?UTF-8?q?=D0=B9=20=D0=BB=D0=B0=D0=B1=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Потихоньку делаю --- CloudDevelopment.sln | 66 +++++++++++++++++++ CompanyEmployee.AppHost/AppHost.cs | 52 +++++++++++++-- .../CompanyEmployee.AppHost.csproj | 1 + .../CompanyEmployee.FileService.csproj | 19 ++++++ CompanyEmployee.FileService/Program.cs | 6 ++ .../Properties/launchSettings.json | 38 +++++++++++ .../appsettings.Development.json | 8 +++ CompanyEmployee.FileService/appsettings.json | 9 +++ 8 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 CompanyEmployee.FileService/CompanyEmployee.FileService.csproj create mode 100644 CompanyEmployee.FileService/Program.cs create mode 100644 CompanyEmployee.FileService/Properties/launchSettings.json create mode 100644 CompanyEmployee.FileService/appsettings.Development.json create mode 100644 CompanyEmployee.FileService/appsettings.json diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index edbb81dc..107a3c4d 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -15,36 +15,102 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.Api", "Comp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.Gateway", "CompanyEmployee.Gateway\CompanyEmployee.Gateway.csproj", "{73C5B926-EAC3-32E1-1AD5-911888B0E715}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.FileService", "CompanyEmployee.FileService\CompanyEmployee.FileService.csproj", "{A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|x64.ActiveCfg = Debug|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|x64.Build.0 = Debug|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|x86.ActiveCfg = Debug|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|x86.Build.0 = Debug|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|x64.ActiveCfg = Release|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|x64.Build.0 = Release|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|x86.ActiveCfg = Release|Any CPU + {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|x86.Build.0 = Release|Any CPU {069756DA-EFFA-4835-B69C-0849C48BE473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {069756DA-EFFA-4835-B69C-0849C48BE473}.Debug|Any CPU.Build.0 = Debug|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Debug|x64.ActiveCfg = Debug|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Debug|x64.Build.0 = Debug|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Debug|x86.ActiveCfg = Debug|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Debug|x86.Build.0 = Debug|Any CPU {069756DA-EFFA-4835-B69C-0849C48BE473}.Release|Any CPU.ActiveCfg = Release|Any CPU {069756DA-EFFA-4835-B69C-0849C48BE473}.Release|Any CPU.Build.0 = Release|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Release|x64.ActiveCfg = Release|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Release|x64.Build.0 = Release|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Release|x86.ActiveCfg = Release|Any CPU + {069756DA-EFFA-4835-B69C-0849C48BE473}.Release|x86.Build.0 = Release|Any CPU {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Debug|x64.ActiveCfg = Debug|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Debug|x64.Build.0 = Debug|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Debug|x86.ActiveCfg = Debug|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Debug|x86.Build.0 = Debug|Any CPU {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Release|Any CPU.ActiveCfg = Release|Any CPU {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Release|Any CPU.Build.0 = Release|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Release|x64.ActiveCfg = Release|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Release|x64.Build.0 = Release|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Release|x86.ActiveCfg = Release|Any CPU + {60C547C0-C951-4270-1D2E-4BB68A5739B6}.Release|x86.Build.0 = Release|Any CPU {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Debug|x64.ActiveCfg = Debug|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Debug|x64.Build.0 = Debug|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Debug|x86.ActiveCfg = Debug|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Debug|x86.Build.0 = Debug|Any CPU {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Release|Any CPU.Build.0 = Release|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Release|x64.ActiveCfg = Release|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Release|x64.Build.0 = Release|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Release|x86.ActiveCfg = Release|Any CPU + {FD5B46C8-0F5C-493A-B5FF-708AEA44AD3D}.Release|x86.Build.0 = Release|Any CPU {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Debug|x64.ActiveCfg = Debug|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Debug|x64.Build.0 = Debug|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Debug|x86.ActiveCfg = Debug|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Debug|x86.Build.0 = Debug|Any CPU {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Release|Any CPU.ActiveCfg = Release|Any CPU {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Release|Any CPU.Build.0 = Release|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Release|x64.ActiveCfg = Release|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Release|x64.Build.0 = Release|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Release|x86.ActiveCfg = Release|Any CPU + {EEC6E8C9-9951-4CE6-DBC8-FEDF498A759B}.Release|x86.Build.0 = Release|Any CPU {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Debug|Any CPU.Build.0 = Debug|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Debug|x64.ActiveCfg = Debug|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Debug|x64.Build.0 = Debug|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Debug|x86.ActiveCfg = Debug|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Debug|x86.Build.0 = Debug|Any CPU {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Release|Any CPU.ActiveCfg = Release|Any CPU {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Release|Any CPU.Build.0 = Release|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Release|x64.ActiveCfg = Release|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Release|x64.Build.0 = Release|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Release|x86.ActiveCfg = Release|Any CPU + {73C5B926-EAC3-32E1-1AD5-911888B0E715}.Release|x86.Build.0 = Release|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Debug|x64.ActiveCfg = Debug|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Debug|x64.Build.0 = Debug|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Debug|x86.ActiveCfg = Debug|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Debug|x86.Build.0 = Debug|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Release|Any CPU.Build.0 = Release|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Release|x64.ActiveCfg = Release|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Release|x64.Build.0 = Release|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Release|x86.ActiveCfg = Release|Any CPU + {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index 9622058d..afbc2353 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -1,10 +1,26 @@ +using Aspire.Hosting; + var builder = DistributedApplication.CreateBuilder(args); var redis = builder.AddRedis("redis") - .WithRedisCommander(containerName: "redis-commander"); + .WithRedisCommander(); + +var minio = builder.AddContainer("minio", "minio/minio") + .WithArgs("server", "/data", "--console-address", ":9001") + .WithEnvironment("MINIO_ROOT_USER", "minioadmin") + .WithEnvironment("MINIO_ROOT_PASSWORD", "minioadmin") + .WithEndpoint(port: 9000, targetPort: 9000, name: "api", scheme: "http") + .WithEndpoint(port: 9001, targetPort: 9001, name: "console", scheme: "http"); + +var localstack = builder.AddContainer("localstack", "localstack/localstack:latest") + .WithEnvironment("SERVICES", "sns,sqs") + .WithEnvironment("AWS_ACCESS_KEY_ID", "test") + .WithEnvironment("AWS_SECRET_ACCESS_KEY", "test") + .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1") + .WithEndpoint(port: 4566, targetPort: 4566, name: "api", scheme: "http"); var gateway = builder.AddProject("gateway") - .WithEndpoint("https", e => e.Port = 7001) + .WithEndpoint("https", e => e.Port = 7000) .WithExternalHttpEndpoints(); const int startApiPort = 6001; @@ -14,14 +30,42 @@ { var port = startApiPort + i; var api = builder.AddProject($"api-{i + 1}") - .WithEndpoint("https", e => e.Port = port) .WithReference(redis) + .WithReference(minio) + .WithReference(localstack) + .WithEndpoint("https", e => e.Port = port) .WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development") - .WaitFor(redis); + .WithEnvironment("AWS__ServiceURL", "http://localhost:4566") + .WithEnvironment("AWS__AccessKeyId", "test") + .WithEnvironment("AWS__SecretAccessKey", "test") + .WithEnvironment("AWS__Region", "us-east-1") + .WithEnvironment("SNS__TopicArn", "arn:aws:sns:us-east-1:000000000000:employee-events") + .WithEnvironment("STORAGE__Endpoint", "http://localhost:9000") + .WithEnvironment("STORAGE__AccessKey", "minioadmin") + .WithEnvironment("STORAGE__AccessSecret", "minioadmin") + .WithEnvironment("STORAGE__BucketName", "employee-data") + .WaitFor(redis) + .WaitFor(minio) + .WaitFor(localstack); gateway.WaitFor(api); } +var fileService = builder.AddProject("fileservice") + .WithReference(localstack) + .WithReference(minio) + .WithEnvironment("AWS__ServiceURL", "http://localhost:4566") + .WithEnvironment("AWS__AccessKeyId", "test") + .WithEnvironment("AWS__SecretAccessKey", "test") + .WithEnvironment("AWS__Region", "us-east-1") + .WithEnvironment("SNS__TopicArn", "arn:aws:sns:us-east-1:000000000000:employee-events") + .WithEnvironment("STORAGE__Endpoint", "http://localhost:9000") + .WithEnvironment("STORAGE__AccessKey", "minioadmin") + .WithEnvironment("STORAGE__AccessSecret", "minioadmin") + .WithEnvironment("STORAGE__BucketName", "employee-data") + .WaitFor(localstack) + .WaitFor(minio); + var client = builder.AddProject("client") .WithReference(gateway) .WaitFor(gateway); diff --git a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj index 60dac8dc..aa5ee317 100644 --- a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj +++ b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj @@ -13,6 +13,7 @@ + diff --git a/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj b/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj new file mode 100644 index 00000000..ab75fe72 --- /dev/null +++ b/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/CompanyEmployee.FileService/Program.cs b/CompanyEmployee.FileService/Program.cs new file mode 100644 index 00000000..1760df1d --- /dev/null +++ b/CompanyEmployee.FileService/Program.cs @@ -0,0 +1,6 @@ +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +app.MapGet("/", () => "Hello World!"); + +app.Run(); diff --git a/CompanyEmployee.FileService/Properties/launchSettings.json b/CompanyEmployee.FileService/Properties/launchSettings.json new file mode 100644 index 00000000..75948cf6 --- /dev/null +++ b/CompanyEmployee.FileService/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:1928", + "sslPort": 44325 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5194", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7277;http://localhost:5194", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/CompanyEmployee.FileService/appsettings.Development.json b/CompanyEmployee.FileService/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/CompanyEmployee.FileService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CompanyEmployee.FileService/appsettings.json b/CompanyEmployee.FileService/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/CompanyEmployee.FileService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From ad91d6a695ec6e893a89ab6038c588c86a9c23ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 3 Apr 2026 19:59:07 +0400 Subject: [PATCH 18/24] =?UTF-8?q?=D0=92=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5?= =?UTF-8?q?=D1=81=D1=81=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CompanyEmployee.FileService/Program.cs | 17 +++++++++++-- .../Services/IStorageService.cs | 22 +++++++++++++++++ .../Services/Settings.cs | 24 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 CompanyEmployee.FileService/Services/IStorageService.cs create mode 100644 CompanyEmployee.FileService/Services/Settings.cs diff --git a/CompanyEmployee.FileService/Program.cs b/CompanyEmployee.FileService/Program.cs index 1760df1d..62ab8d63 100644 --- a/CompanyEmployee.FileService/Program.cs +++ b/CompanyEmployee.FileService/Program.cs @@ -1,6 +1,19 @@ +using CompanyEmployee.FileService.Services; +using CompanyEmployee.ServiceDefaults; + var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.Configure(builder.Configuration.GetSection("SNS")); +builder.Services.Configure(builder.Configuration.GetSection("Storage")); + +builder.Services.AddSingleton(); +builder.Services.AddHostedService(); + var app = builder.Build(); -app.MapGet("/", () => "Hello World!"); +app.MapDefaultEndpoints(); +app.MapGet("/health", () => "Healthy"); -app.Run(); +app.Run(); \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/IStorageService.cs b/CompanyEmployee.FileService/Services/IStorageService.cs new file mode 100644 index 00000000..275c61f6 --- /dev/null +++ b/CompanyEmployee.FileService/Services/IStorageService.cs @@ -0,0 +1,22 @@ +namespace CompanyEmployee.FileService.Services; + +/// +/// Сервис для работы с объектным хранилищем. +/// +public interface IStorageService +{ + /// + /// Сохраняет файл в хранилище. + /// + public Task SaveFileAsync(string key, byte[] content, string contentType = "application/json"); + + /// + /// Получает файл из хранилища. + /// + public Task GetFileAsync(string key); + + /// + /// Проверяет существование файла. + /// + public Task FileExistsAsync(string key); +} \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/Settings.cs b/CompanyEmployee.FileService/Services/Settings.cs new file mode 100644 index 00000000..da5a0c3b --- /dev/null +++ b/CompanyEmployee.FileService/Services/Settings.cs @@ -0,0 +1,24 @@ +namespace CompanyEmployee.FileService.Services; + +/// +/// Настройки SNS/SQS. +/// +public class SnsSettings +{ + public string ServiceURL { get; set; } = "http://localhost:4566"; + public string AccessKeyId { get; set; } = "test"; + public string SecretAccessKey { get; set; } = "test"; + public string Region { get; set; } = "us-east-1"; + public string TopicArn { get; set; } = "arn:aws:sns:us-east-1:000000000000:employee-events"; +} + +/// +/// Настройки объектного хранилища. +/// +public class StorageSettings +{ + public string Endpoint { get; set; } = "http://localhost:9000"; + public string AccessKey { get; set; } = "minioadmin"; + public string AccessSecret { get; set; } = "minioadmin"; + public string BucketName { get; set; } = "employee-data"; +} \ No newline at end of file From 3b7d3ba5a26925b886ef63f0e7799861d2e8ca45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 3 Apr 2026 21:25:52 +0400 Subject: [PATCH 19/24] =?UTF-8?q?=D0=9D=D0=B5=D0=BC=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BE=D1=88=D0=B8=D0=B1=D1=81=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Написал всякой фигни, понял, что не работает, теперь переделываю --- CloudDevelopment.sln | 14 ++++ .../CompanyEmployee.Api.csproj | 1 + CompanyEmployee.Api/Program.cs | 19 ++++- CompanyEmployee.Api/Services/SnsSettings.cs | 13 ++++ CompanyEmployee.AppHost/AppHost.cs | 61 +++++++-------- .../CompanyEmployee.AppHost.csproj | 2 +- .../CompanyEmployee.FileService.csproj | 5 +- .../Consumers/EmployeeGeneratedConsumer.cs | 36 +++++++++ CompanyEmployee.FileService/Program.cs | 42 +++++++++- .../Services/IS3FileStorage.cs | 6 ++ .../Services/IStorageService.cs | 22 ------ .../Services/S3FileStorage.cs | 44 +++++++++++ .../Services/Settings.cs | 24 ------ CompanyEmployee.FileService/appsettings.json | 17 ++++- CompanyEmployee.IntegrationTests/BasicTest.cs | 76 +++++++++++++++++++ .../CompanyEmployee.IntegrationTests.csproj | 31 ++++++++ 16 files changed, 325 insertions(+), 88 deletions(-) create mode 100644 CompanyEmployee.Api/Services/SnsSettings.cs create mode 100644 CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs create mode 100644 CompanyEmployee.FileService/Services/IS3FileStorage.cs delete mode 100644 CompanyEmployee.FileService/Services/IStorageService.cs create mode 100644 CompanyEmployee.FileService/Services/S3FileStorage.cs delete mode 100644 CompanyEmployee.FileService/Services/Settings.cs create mode 100644 CompanyEmployee.IntegrationTests/BasicTest.cs create mode 100644 CompanyEmployee.IntegrationTests/CompanyEmployee.IntegrationTests.csproj diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln index 107a3c4d..e318f998 100644 --- a/CloudDevelopment.sln +++ b/CloudDevelopment.sln @@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.Gateway", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.FileService", "CompanyEmployee.FileService\CompanyEmployee.FileService.csproj", "{A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyEmployee.IntegrationTests", "CompanyEmployee.IntegrationTests\CompanyEmployee.IntegrationTests.csproj", "{5B3E2130-4B0A-414D-B075-7AF50B7D9B55}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -111,6 +113,18 @@ Global {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Release|x64.Build.0 = Release|Any CPU {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Release|x86.ActiveCfg = Release|Any CPU {A276F6D0-18D6-4A2B-A8FE-C03CA0694DEE}.Release|x86.Build.0 = Release|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Debug|x64.Build.0 = Debug|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Debug|x86.Build.0 = Debug|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Release|Any CPU.Build.0 = Release|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Release|x64.ActiveCfg = Release|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Release|x64.Build.0 = Release|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Release|x86.ActiveCfg = Release|Any CPU + {5B3E2130-4B0A-414D-B075-7AF50B7D9B55}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CompanyEmployee.Api/CompanyEmployee.Api.csproj b/CompanyEmployee.Api/CompanyEmployee.Api.csproj index f128ff9c..09ccbda3 100644 --- a/CompanyEmployee.Api/CompanyEmployee.Api.csproj +++ b/CompanyEmployee.Api/CompanyEmployee.Api.csproj @@ -8,6 +8,7 @@ + diff --git a/CompanyEmployee.Api/Program.cs b/CompanyEmployee.Api/Program.cs index 99a787de..65f7ef04 100644 --- a/CompanyEmployee.Api/Program.cs +++ b/CompanyEmployee.Api/Program.cs @@ -1,10 +1,27 @@ -using CompanyEmployee.Api.Services; +using Amazon.SimpleNotificationService; +using CompanyEmployee.Api.Services; using CompanyEmployee.ServiceDefaults; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); builder.AddRedisDistributedCache("redis"); + +builder.Services.Configure(builder.Configuration.GetSection("SNS")); + +var snsConfig = new AmazonSimpleNotificationServiceConfig +{ + ServiceURL = builder.Configuration["AWS:ServiceURL"] ?? "http://localhost:4566", + AuthenticationRegion = builder.Configuration["AWS:Region"] ?? "us-east-1", + UseHttp = true +}; + +builder.Services.AddSingleton(sp => + new AmazonSimpleNotificationServiceClient( + builder.Configuration["AWS:AccessKeyId"] ?? "test", + builder.Configuration["AWS:SecretAccessKey"] ?? "test", + snsConfig)); + builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); diff --git a/CompanyEmployee.Api/Services/SnsSettings.cs b/CompanyEmployee.Api/Services/SnsSettings.cs new file mode 100644 index 00000000..eae38e13 --- /dev/null +++ b/CompanyEmployee.Api/Services/SnsSettings.cs @@ -0,0 +1,13 @@ +namespace CompanyEmployee.Api.Services; + +/// +/// Настройки SNS. +/// +public class SnsSettings +{ + public string ServiceURL { get; set; } = "http://localhost:4566"; + public string AccessKeyId { get; set; } = "test"; + public string SecretAccessKey { get; set; } = "test"; + public string Region { get; set; } = "us-east-1"; + public string TopicArn { get; set; } = "arn:aws:sns:us-east-1:000000000000:employee-events"; +} \ No newline at end of file diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index afbc2353..45cf1375 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -5,19 +5,28 @@ var redis = builder.AddRedis("redis") .WithRedisCommander(); +var localstack = builder.AddContainer("localstack", "localstack/localstack") + .WithEndpoint(port: 4566, targetPort: 4566, name: "localstack", scheme: "http") + .WithEnvironment("SERVICES", "sns,sqs") + .WithEnvironment("DEFAULT_REGION", "us-east-1") + .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1") + .WithHttpHealthCheck(path: "/_localstack/health", endpointName: "localstack"); + var minio = builder.AddContainer("minio", "minio/minio") .WithArgs("server", "/data", "--console-address", ":9001") .WithEnvironment("MINIO_ROOT_USER", "minioadmin") .WithEnvironment("MINIO_ROOT_PASSWORD", "minioadmin") .WithEndpoint(port: 9000, targetPort: 9000, name: "api", scheme: "http") - .WithEndpoint(port: 9001, targetPort: 9001, name: "console", scheme: "http"); + .WithEndpoint(port: 9001, targetPort: 9001, name: "console", scheme: "http") + .WithHttpHealthCheck(path: "/minio/health/ready", endpointName: "api"); -var localstack = builder.AddContainer("localstack", "localstack/localstack:latest") - .WithEnvironment("SERVICES", "sns,sqs") - .WithEnvironment("AWS_ACCESS_KEY_ID", "test") - .WithEnvironment("AWS_SECRET_ACCESS_KEY", "test") - .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1") - .WithEndpoint(port: 4566, targetPort: 4566, name: "api", scheme: "http"); +var fileService = builder.AddProject("fileservice") + .WithReference(localstack.GetEndpoint("localstack")) + .WaitFor(localstack) + .WithReference(minio.GetEndpoint("api")) + .WaitFor(minio) + .WithEnvironment("AWS__ServiceURL", localstack.GetEndpoint("localstack")) + .WithEnvironment("STORAGE__Endpoint", minio.GetEndpoint("api")); var gateway = builder.AddProject("gateway") .WithEndpoint("https", e => e.Port = 7000) @@ -25,46 +34,34 @@ const int startApiPort = 6001; const int replicaCount = 5; +var apiReplicas = new List>(); for (var i = 0; i < replicaCount; i++) { var port = startApiPort + i; var api = builder.AddProject($"api-{i + 1}") .WithReference(redis) - .WithReference(minio) - .WithReference(localstack) + .WaitFor(redis) + .WithReference(localstack.GetEndpoint("localstack")) + .WaitFor(localstack) + .WaitFor(fileService) .WithEndpoint("https", e => e.Port = port) .WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development") - .WithEnvironment("AWS__ServiceURL", "http://localhost:4566") - .WithEnvironment("AWS__AccessKeyId", "test") - .WithEnvironment("AWS__SecretAccessKey", "test") - .WithEnvironment("AWS__Region", "us-east-1") + .WithEnvironment("AWS__ServiceURL", localstack.GetEndpoint("localstack")) .WithEnvironment("SNS__TopicArn", "arn:aws:sns:us-east-1:000000000000:employee-events") - .WithEnvironment("STORAGE__Endpoint", "http://localhost:9000") + .WithEnvironment("STORAGE__Endpoint", minio.GetEndpoint("api")) .WithEnvironment("STORAGE__AccessKey", "minioadmin") .WithEnvironment("STORAGE__AccessSecret", "minioadmin") - .WithEnvironment("STORAGE__BucketName", "employee-data") - .WaitFor(redis) - .WaitFor(minio) - .WaitFor(localstack); + .WithEnvironment("STORAGE__BucketName", "employee-data"); + apiReplicas.Add(api); gateway.WaitFor(api); } -var fileService = builder.AddProject("fileservice") - .WithReference(localstack) - .WithReference(minio) - .WithEnvironment("AWS__ServiceURL", "http://localhost:4566") - .WithEnvironment("AWS__AccessKeyId", "test") - .WithEnvironment("AWS__SecretAccessKey", "test") - .WithEnvironment("AWS__Region", "us-east-1") - .WithEnvironment("SNS__TopicArn", "arn:aws:sns:us-east-1:000000000000:employee-events") - .WithEnvironment("STORAGE__Endpoint", "http://localhost:9000") - .WithEnvironment("STORAGE__AccessKey", "minioadmin") - .WithEnvironment("STORAGE__AccessSecret", "minioadmin") - .WithEnvironment("STORAGE__BucketName", "employee-data") - .WaitFor(localstack) - .WaitFor(minio); +foreach (var replica in apiReplicas) +{ + gateway.WithReference(replica); +} var client = builder.AddProject("client") .WithReference(gateway) diff --git a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj index aa5ee317..2f38984d 100644 --- a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj +++ b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj @@ -13,13 +13,13 @@ - + diff --git a/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj b/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj index ab75fe72..24d55b36 100644 --- a/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj +++ b/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj @@ -7,8 +7,9 @@ - - + + + diff --git a/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs b/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs new file mode 100644 index 00000000..03af09a2 --- /dev/null +++ b/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs @@ -0,0 +1,36 @@ +using MassTransit; +using CompanyEmployee.Domain.Entity; +using CompanyEmployee.FileService.Services; +using System.Text.Json; + +namespace CompanyEmployee.FileService.Consumers; + +/// +/// Консьюмер для обработки сообщений о генерации сотрудника. +/// +public class EmployeeGeneratedConsumer : IConsumer +{ + private readonly IS3FileStorage _storage; + private readonly string _bucketName; + private readonly ILogger _logger; + + public EmployeeGeneratedConsumer( + IS3FileStorage storage, + string bucketName, + ILogger logger) + { + _storage = storage; + _bucketName = bucketName; + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + var employee = context.Message; + var fileName = $"employee_{employee.Id}_{DateTime.Now:yyyyMMddHHmmss}.json"; + var content = JsonSerializer.SerializeToUtf8Bytes(employee); + + await _storage.UploadFileAsync(_bucketName, fileName, content); + _logger.LogInformation("Сотрудник {Id} сохранён в S3 как {FileName}", employee.Id, fileName); + } +} \ No newline at end of file diff --git a/CompanyEmployee.FileService/Program.cs b/CompanyEmployee.FileService/Program.cs index 62ab8d63..d0f5f833 100644 --- a/CompanyEmployee.FileService/Program.cs +++ b/CompanyEmployee.FileService/Program.cs @@ -1,15 +1,49 @@ +using Amazon.S3; +using Amazon.SimpleNotificationService; +using Amazon.SQS; +using CompanyEmployee.FileService.Consumers; using CompanyEmployee.FileService.Services; using CompanyEmployee.ServiceDefaults; +using MassTransit; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -builder.Services.Configure(builder.Configuration.GetSection("SNS")); -builder.Services.Configure(builder.Configuration.GetSection("Storage")); +var awsServiceUrl = builder.Configuration["Aws:ServiceUrl"] ?? "http://localhost:4566"; +var awsRegion = builder.Configuration["Aws:Region"] ?? "us-east-1"; +var awsAccessKey = builder.Configuration["Aws:AccessKey"] ?? "test"; +var awsSecretKey = builder.Configuration["Aws:SecretKey"] ?? "test"; +var bucketName = builder.Configuration["Aws:BucketName"] ?? "employee-data"; -builder.Services.AddSingleton(); -builder.Services.AddHostedService(); +builder.Services.AddSingleton(_ => new AmazonS3Client( + awsAccessKey, awsSecretKey, + new AmazonS3Config + { + ServiceURL = awsServiceUrl, + ForcePathStyle = true + })); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(bucketName); + +builder.Services.AddMassTransit(x => +{ + x.AddConsumer(); + + x.UsingAmazonSqs((context, cfg) => + { + cfg.Host(awsRegion, h => + { + h.Config(new AmazonSQSConfig { ServiceURL = awsServiceUrl }); + h.Config(new AmazonSimpleNotificationServiceConfig { ServiceURL = awsServiceUrl }); + h.AccessKey(awsAccessKey); + h.SecretKey(awsSecretKey); + }); + + cfg.ConfigureEndpoints(context); + }); +}); var app = builder.Build(); diff --git a/CompanyEmployee.FileService/Services/IS3FileStorage.cs b/CompanyEmployee.FileService/Services/IS3FileStorage.cs new file mode 100644 index 00000000..d090cd4d --- /dev/null +++ b/CompanyEmployee.FileService/Services/IS3FileStorage.cs @@ -0,0 +1,6 @@ +namespace CompanyEmployee.FileService.Services; + +public interface IS3FileStorage +{ + public Task UploadFileAsync(string bucketName, string key, byte[] content); +} \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/IStorageService.cs b/CompanyEmployee.FileService/Services/IStorageService.cs deleted file mode 100644 index 275c61f6..00000000 --- a/CompanyEmployee.FileService/Services/IStorageService.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace CompanyEmployee.FileService.Services; - -/// -/// Сервис для работы с объектным хранилищем. -/// -public interface IStorageService -{ - /// - /// Сохраняет файл в хранилище. - /// - public Task SaveFileAsync(string key, byte[] content, string contentType = "application/json"); - - /// - /// Получает файл из хранилища. - /// - public Task GetFileAsync(string key); - - /// - /// Проверяет существование файла. - /// - public Task FileExistsAsync(string key); -} \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/S3FileStorage.cs b/CompanyEmployee.FileService/Services/S3FileStorage.cs new file mode 100644 index 00000000..65f9ee39 --- /dev/null +++ b/CompanyEmployee.FileService/Services/S3FileStorage.cs @@ -0,0 +1,44 @@ +using Amazon.S3; +using Amazon.S3.Model; + +namespace CompanyEmployee.FileService.Services; + +public class S3FileStorage : IS3FileStorage +{ + private readonly IAmazonS3 _s3Client; + private readonly ILogger _logger; + + public S3FileStorage(IAmazonS3 s3Client, ILogger logger) + { + _s3Client = s3Client; + _logger = logger; + } + + public async Task UploadFileAsync(string bucketName, string key, byte[] content) + { + try + { + var bucketExists = await _s3Client.DoesS3BucketExistAsync(bucketName); + if (!bucketExists) + { + await _s3Client.PutBucketAsync(new PutBucketRequest { BucketName = bucketName }); + } + + using var stream = new MemoryStream(content); + await _s3Client.PutObjectAsync(new PutObjectRequest + { + BucketName = bucketName, + Key = key, + InputStream = stream, + ContentType = "application/json" + }); + + _logger.LogInformation("Файл {Key} загружен в {BucketName}", key, bucketName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Ошибка загрузки файла {Key}", key); + throw; + } + } +} \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/Settings.cs b/CompanyEmployee.FileService/Services/Settings.cs deleted file mode 100644 index da5a0c3b..00000000 --- a/CompanyEmployee.FileService/Services/Settings.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace CompanyEmployee.FileService.Services; - -/// -/// Настройки SNS/SQS. -/// -public class SnsSettings -{ - public string ServiceURL { get; set; } = "http://localhost:4566"; - public string AccessKeyId { get; set; } = "test"; - public string SecretAccessKey { get; set; } = "test"; - public string Region { get; set; } = "us-east-1"; - public string TopicArn { get; set; } = "arn:aws:sns:us-east-1:000000000000:employee-events"; -} - -/// -/// Настройки объектного хранилища. -/// -public class StorageSettings -{ - public string Endpoint { get; set; } = "http://localhost:9000"; - public string AccessKey { get; set; } = "minioadmin"; - public string AccessSecret { get; set; } = "minioadmin"; - public string BucketName { get; set; } = "employee-data"; -} \ No newline at end of file diff --git a/CompanyEmployee.FileService/appsettings.json b/CompanyEmployee.FileService/appsettings.json index 10f68b8c..a60e0f77 100644 --- a/CompanyEmployee.FileService/appsettings.json +++ b/CompanyEmployee.FileService/appsettings.json @@ -5,5 +5,18 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} + "AllowedHosts": "*", + "SNS": { + "ServiceURL": "http://localhost:4566", + "AccessKeyId": "test", + "SecretAccessKey": "test", + "Region": "us-east-1", + "TopicArn": "arn:aws:sns:us-east-1:000000000000:employee-events" + }, + "Storage": { + "Endpoint": "localhost:9000", + "AccessKey": "minioadmin", + "AccessSecret": "minioadmin", + "BucketName": "employee-data" + } +} \ No newline at end of file diff --git a/CompanyEmployee.IntegrationTests/BasicTest.cs b/CompanyEmployee.IntegrationTests/BasicTest.cs new file mode 100644 index 00000000..f918ecc7 --- /dev/null +++ b/CompanyEmployee.IntegrationTests/BasicTest.cs @@ -0,0 +1,76 @@ +using CompanyEmployee.Domain.Entity; +using System.Text.Json; + +namespace CompanyEmployee.IntegrationTests; + +/// +/// . +/// +public class BasicTests +{ + private readonly HttpClient _client; + + public BasicTests() + { + _client = new HttpClient + { + BaseAddress = new Uri("https://localhost:6001") + }; + } + + /// + /// , API . + /// + [Fact] + public async Task GetEmployee_ShouldReturnEmployee() + { + var response = await _client.GetAsync("/api/employee/1"); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + var employee = JsonSerializer.Deserialize(content, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + Assert.NotNull(employee); + Assert.Equal(1, employee.Id); + Assert.False(string.IsNullOrEmpty(employee.FullName)); + } + + /// + /// . + /// + [Fact] + public async Task SameEmployeeId_ShouldReturnSameData() + { + var response1 = await _client.GetAsync("/api/employee/2"); + var content1 = await response1.Content.ReadAsStringAsync(); + + var response2 = await _client.GetAsync("/api/employee/2"); + var content2 = await response2.Content.ReadAsStringAsync(); + + Assert.Equal(content1, content2); + } + + /// + /// , ID . + /// + [Fact] + public async Task DifferentIds_ShouldGenerateDifferentEmployees() + { + var response1 = await _client.GetAsync("/api/employee/10"); + var employee1 = JsonSerializer.Deserialize( + await response1.Content.ReadAsStringAsync(), + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + var response2 = await _client.GetAsync("/api/employee/20"); + var employee2 = JsonSerializer.Deserialize( + await response2.Content.ReadAsStringAsync(), + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + Assert.NotNull(employee1); + Assert.NotNull(employee2); + Assert.NotEqual(employee1.FullName, employee2.FullName); + } +} \ No newline at end of file diff --git a/CompanyEmployee.IntegrationTests/CompanyEmployee.IntegrationTests.csproj b/CompanyEmployee.IntegrationTests/CompanyEmployee.IntegrationTests.csproj new file mode 100644 index 00000000..72b8529e --- /dev/null +++ b/CompanyEmployee.IntegrationTests/CompanyEmployee.IntegrationTests.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + From bc476903471de08927c74924d542d8391d9e7a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Fri, 3 Apr 2026 22:51:48 +0400 Subject: [PATCH 20/24] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0?= =?UTF-8?q?=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вроде, все работает, осталось немного доделать --- Client.Wasm/wwwroot/appsettings.json | 2 +- .../CompanyEmployee.Api.csproj | 2 +- CompanyEmployee.Api/Program.cs | 41 +++++---- .../Services/EmployeeService.cs | 83 ++++++++++++++----- CompanyEmployee.Api/Services/SnsSettings.cs | 13 --- CompanyEmployee.Api/appsettings.json | 13 ++- CompanyEmployee.AppHost/AppHost.cs | 40 ++++----- .../CompanyEmployee.FileService.csproj | 1 - CompanyEmployee.FileService/Program.cs | 14 ++-- .../appsettings.Development.json | 11 ++- CompanyEmployee.FileService/appsettings.json | 12 +-- 11 files changed, 138 insertions(+), 94 deletions(-) delete mode 100644 CompanyEmployee.Api/Services/SnsSettings.cs diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json index 8e21597f..cc67cbd9 100644 --- a/Client.Wasm/wwwroot/appsettings.json +++ b/Client.Wasm/wwwroot/appsettings.json @@ -6,5 +6,5 @@ } }, "AllowedHosts": "*", - "BaseAddress": "https://localhost:7001/api/employee" + "BaseAddress": "https://localhost:7000/api/employee" } diff --git a/CompanyEmployee.Api/CompanyEmployee.Api.csproj b/CompanyEmployee.Api/CompanyEmployee.Api.csproj index 09ccbda3..6210ba34 100644 --- a/CompanyEmployee.Api/CompanyEmployee.Api.csproj +++ b/CompanyEmployee.Api/CompanyEmployee.Api.csproj @@ -8,8 +8,8 @@ - + diff --git a/CompanyEmployee.Api/Program.cs b/CompanyEmployee.Api/Program.cs index 65f7ef04..32df462c 100644 --- a/CompanyEmployee.Api/Program.cs +++ b/CompanyEmployee.Api/Program.cs @@ -1,35 +1,43 @@ using Amazon.SimpleNotificationService; +using Amazon.SQS; using CompanyEmployee.Api.Services; using CompanyEmployee.ServiceDefaults; +using MassTransit; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); builder.AddRedisDistributedCache("redis"); -builder.Services.Configure(builder.Configuration.GetSection("SNS")); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); -var snsConfig = new AmazonSimpleNotificationServiceConfig -{ - ServiceURL = builder.Configuration["AWS:ServiceURL"] ?? "http://localhost:4566", - AuthenticationRegion = builder.Configuration["AWS:Region"] ?? "us-east-1", - UseHttp = true -}; +var awsServiceUrl = builder.Configuration["AWS:ServiceURL"] ?? "http://localhost:4566"; +var awsRegion = builder.Configuration["AWS:Region"] ?? "us-east-1"; +var awsAccessKey = builder.Configuration["AWS:AccessKeyId"] ?? "test"; +var awsSecretKey = builder.Configuration["AWS:SecretAccessKey"] ?? "test"; -builder.Services.AddSingleton(sp => - new AmazonSimpleNotificationServiceClient( - builder.Configuration["AWS:AccessKeyId"] ?? "test", - builder.Configuration["AWS:SecretAccessKey"] ?? "test", - snsConfig)); +builder.Services.AddMassTransit(x => +{ + x.UsingAmazonSqs((context, cfg) => + { + cfg.Host(awsRegion, h => + { + h.Config(new AmazonSQSConfig { ServiceURL = awsServiceUrl }); + h.Config(new AmazonSimpleNotificationServiceConfig { ServiceURL = awsServiceUrl }); + h.AccessKey(awsAccessKey); + h.SecretKey(awsSecretKey); + }); + }); +}); + +builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); -builder.Services.AddSingleton(); -builder.Services.AddSingleton(); -builder.Services.AddScoped(); - var app = builder.Build(); if (app.Environment.IsDevelopment()) @@ -42,4 +50,5 @@ app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); + app.Run(); \ No newline at end of file diff --git a/CompanyEmployee.Api/Services/EmployeeService.cs b/CompanyEmployee.Api/Services/EmployeeService.cs index 11b29566..61a6a906 100644 --- a/CompanyEmployee.Api/Services/EmployeeService.cs +++ b/CompanyEmployee.Api/Services/EmployeeService.cs @@ -1,39 +1,78 @@ -using CompanyEmployee.Domain.Entity; +using System.Text.Json; +using CompanyEmployee.Domain.Entity; using Microsoft.Extensions.Caching.Distributed; +using MassTransit; namespace CompanyEmployee.Api.Services; -/// -/// Бизнес-логика работы с сотрудниками. -/// -/// Генератор сотрудников. -/// Сервис кэширования. -/// Логгер. -public class EmployeeService( - IEmployeeGenerator generator, - ICacheService cache, - ILogger logger) : IEmployeeService +public class EmployeeService : IEmployeeService { - private readonly DistributedCacheEntryOptions _cacheOptions = new() + private readonly IEmployeeGenerator _generator; + private readonly IDistributedCache _cache; + private readonly IPublishEndpoint _publishEndpoint; + private readonly ILogger _logger; + private readonly DistributedCacheEntryOptions _cacheOptions; + + public EmployeeService( + IEmployeeGenerator generator, + IDistributedCache cache, + IPublishEndpoint publishEndpoint, + ILogger logger) { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) - }; + _generator = generator; + _cache = cache; + _publishEndpoint = publishEndpoint; + _logger = logger; + _cacheOptions = new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) + }; + } - /// public async Task GetEmployeeAsync(int id, CancellationToken cancellationToken = default) { var cacheKey = $"employee:{id}"; - var employee = await cache.GetAsync(cacheKey, cancellationToken); - if (employee != null) + Employee? employee = null; + + try { - logger.LogInformation("Сотрудник с ID {Id} найден в кэше", id); - return employee; + var cachedJson = await _cache.GetStringAsync(cacheKey, cancellationToken); + if (cachedJson != null) + { + _logger.LogInformation("Сотрудник с ID {Id} найден в кэше", id); + employee = JsonSerializer.Deserialize(cachedJson); + return employee; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Ошибка при получении данных из кэша"); } - logger.LogInformation("Сотрудник с ID {Id} не найден в кэше, генерация нового", id); - employee = generator.Generate(id); + try + { + _logger.LogInformation("Сотрудник с ID {Id} не найден в кэше, генерация нового", id); + employee = _generator.Generate(id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Ошибка при генерации сотрудника с ID {Id}", id); + return null; + } + + try + { + var serialized = JsonSerializer.Serialize(employee); + await _cache.SetStringAsync(cacheKey, serialized, _cacheOptions, cancellationToken); + _logger.LogDebug("Сотрудник с ID {Id} сохранён в кэш", id); - await cache.SetAsync(cacheKey, employee, _cacheOptions, cancellationToken); + await _publishEndpoint.Publish(employee, cancellationToken); + _logger.LogInformation("Сообщение о сотруднике {Id} отправлено в SNS", id); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Не удалось отправить сообщение о сотруднике {Id}", id); + } return employee; } diff --git a/CompanyEmployee.Api/Services/SnsSettings.cs b/CompanyEmployee.Api/Services/SnsSettings.cs deleted file mode 100644 index eae38e13..00000000 --- a/CompanyEmployee.Api/Services/SnsSettings.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace CompanyEmployee.Api.Services; - -/// -/// Настройки SNS. -/// -public class SnsSettings -{ - public string ServiceURL { get; set; } = "http://localhost:4566"; - public string AccessKeyId { get; set; } = "test"; - public string SecretAccessKey { get; set; } = "test"; - public string Region { get; set; } = "us-east-1"; - public string TopicArn { get; set; } = "arn:aws:sns:us-east-1:000000000000:employee-events"; -} \ No newline at end of file diff --git a/CompanyEmployee.Api/appsettings.json b/CompanyEmployee.Api/appsettings.json index 10f68b8c..1361d0b6 100644 --- a/CompanyEmployee.Api/appsettings.json +++ b/CompanyEmployee.Api/appsettings.json @@ -5,5 +5,14 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} + "AllowedHosts": "*", + "AWS": { + "ServiceURL": "http://localhost:4566", + "Region": "us-east-1", + "AccessKeyId": "test", + "SecretAccessKey": "test" + }, + "SNS": { + "TopicArn": "arn:aws:sns:us-east-1:000000000000:employee-events" + } +} \ No newline at end of file diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index 45cf1375..c8573a42 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -5,28 +5,26 @@ var redis = builder.AddRedis("redis") .WithRedisCommander(); +var authToken = builder.Configuration["LocalStack:AuthToken"]; + var localstack = builder.AddContainer("localstack", "localstack/localstack") .WithEndpoint(port: 4566, targetPort: 4566, name: "localstack", scheme: "http") - .WithEnvironment("SERVICES", "sns,sqs") + .WithEnvironment("SERVICES", "sns,sqs,s3") .WithEnvironment("DEFAULT_REGION", "us-east-1") - .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1") - .WithHttpHealthCheck(path: "/_localstack/health", endpointName: "localstack"); + .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1"); -var minio = builder.AddContainer("minio", "minio/minio") - .WithArgs("server", "/data", "--console-address", ":9001") - .WithEnvironment("MINIO_ROOT_USER", "minioadmin") - .WithEnvironment("MINIO_ROOT_PASSWORD", "minioadmin") - .WithEndpoint(port: 9000, targetPort: 9000, name: "api", scheme: "http") - .WithEndpoint(port: 9001, targetPort: 9001, name: "console", scheme: "http") - .WithHttpHealthCheck(path: "/minio/health/ready", endpointName: "api"); +if (!string.IsNullOrEmpty(authToken)) +{ + localstack = localstack.WithEnvironment("LOCALSTACK_AUTH_TOKEN", authToken); +} var fileService = builder.AddProject("fileservice") - .WithReference(localstack.GetEndpoint("localstack")) .WaitFor(localstack) - .WithReference(minio.GetEndpoint("api")) - .WaitFor(minio) - .WithEnvironment("AWS__ServiceURL", localstack.GetEndpoint("localstack")) - .WithEnvironment("STORAGE__Endpoint", minio.GetEndpoint("api")); + .WithEnvironment("AWS__ServiceURL", "http://localhost:4566") + .WithEnvironment("AWS__Region", "us-east-1") + .WithEnvironment("AWS__AccessKeyId", "test") + .WithEnvironment("AWS__SecretAccessKey", "test") + .WithEnvironment("S3__BucketName", "employee-data"); var gateway = builder.AddProject("gateway") .WithEndpoint("https", e => e.Port = 7000) @@ -42,17 +40,15 @@ var api = builder.AddProject($"api-{i + 1}") .WithReference(redis) .WaitFor(redis) - .WithReference(localstack.GetEndpoint("localstack")) .WaitFor(localstack) .WaitFor(fileService) .WithEndpoint("https", e => e.Port = port) .WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development") - .WithEnvironment("AWS__ServiceURL", localstack.GetEndpoint("localstack")) - .WithEnvironment("SNS__TopicArn", "arn:aws:sns:us-east-1:000000000000:employee-events") - .WithEnvironment("STORAGE__Endpoint", minio.GetEndpoint("api")) - .WithEnvironment("STORAGE__AccessKey", "minioadmin") - .WithEnvironment("STORAGE__AccessSecret", "minioadmin") - .WithEnvironment("STORAGE__BucketName", "employee-data"); + .WithEnvironment("AWS__ServiceURL", "http://localhost:4566") + .WithEnvironment("AWS__Region", "us-east-1") + .WithEnvironment("AWS__AccessKeyId", "test") + .WithEnvironment("AWS__SecretAccessKey", "test") + .WithEnvironment("SNS__TopicArn", "arn:aws:sns:us-east-1:000000000000:employee-events"); apiReplicas.Add(api); gateway.WaitFor(api); diff --git a/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj b/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj index 24d55b36..719999de 100644 --- a/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj +++ b/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj @@ -8,7 +8,6 @@ - diff --git a/CompanyEmployee.FileService/Program.cs b/CompanyEmployee.FileService/Program.cs index d0f5f833..a74cbb12 100644 --- a/CompanyEmployee.FileService/Program.cs +++ b/CompanyEmployee.FileService/Program.cs @@ -1,20 +1,20 @@ using Amazon.S3; -using Amazon.SimpleNotificationService; -using Amazon.SQS; using CompanyEmployee.FileService.Consumers; using CompanyEmployee.FileService.Services; using CompanyEmployee.ServiceDefaults; using MassTransit; +using Amazon.SimpleNotificationService; +using Amazon.SQS; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); -var awsServiceUrl = builder.Configuration["Aws:ServiceUrl"] ?? "http://localhost:4566"; -var awsRegion = builder.Configuration["Aws:Region"] ?? "us-east-1"; -var awsAccessKey = builder.Configuration["Aws:AccessKey"] ?? "test"; -var awsSecretKey = builder.Configuration["Aws:SecretKey"] ?? "test"; -var bucketName = builder.Configuration["Aws:BucketName"] ?? "employee-data"; +var awsServiceUrl = builder.Configuration["AWS:ServiceURL"] ?? "http://localhost:4566"; +var awsRegion = builder.Configuration["AWS:Region"] ?? "us-east-1"; +var awsAccessKey = builder.Configuration["AWS:AccessKeyId"] ?? "test"; +var awsSecretKey = builder.Configuration["AWS:SecretAccessKey"] ?? "test"; +var bucketName = builder.Configuration["S3:BucketName"] ?? "employee-data"; builder.Services.AddSingleton(_ => new AmazonS3Client( awsAccessKey, awsSecretKey, diff --git a/CompanyEmployee.FileService/appsettings.Development.json b/CompanyEmployee.FileService/appsettings.Development.json index 0c208ae9..c054fc38 100644 --- a/CompanyEmployee.FileService/appsettings.Development.json +++ b/CompanyEmployee.FileService/appsettings.Development.json @@ -4,5 +4,14 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "AWS": { + "ServiceURL": "http://localhost:4566", + "Region": "us-east-1", + "AccessKeyId": "test", + "SecretAccessKey": "test" + }, + "S3": { + "BucketName": "employee-data" } -} +} \ No newline at end of file diff --git a/CompanyEmployee.FileService/appsettings.json b/CompanyEmployee.FileService/appsettings.json index a60e0f77..46ff1a6a 100644 --- a/CompanyEmployee.FileService/appsettings.json +++ b/CompanyEmployee.FileService/appsettings.json @@ -6,17 +6,13 @@ } }, "AllowedHosts": "*", - "SNS": { + "AWS": { "ServiceURL": "http://localhost:4566", - "AccessKeyId": "test", - "SecretAccessKey": "test", "Region": "us-east-1", - "TopicArn": "arn:aws:sns:us-east-1:000000000000:employee-events" + "AccessKeyId": "test", + "SecretAccessKey": "test" }, - "Storage": { - "Endpoint": "localhost:9000", - "AccessKey": "minioadmin", - "AccessSecret": "minioadmin", + "S3": { "BucketName": "employee-data" } } \ No newline at end of file From 4f8812a3edc1f9d666630cef9f0792cc08284a3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Sat, 4 Apr 2026 00:30:42 +0400 Subject: [PATCH 21/24] =?UTF-8?q?=D0=AF=20=D0=B4=D1=83=D1=80=D0=B0=D0=BA..?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Посмотрел не свой вариант и увидел, что у меня Minio, а не Localstack --- .../Controllers/EmployeeController.cs | 2 +- .../Consumers/EmployeeGeneratedConsumer.cs | 12 +-- CompanyEmployee.FileService/Program.cs | 29 ++++- .../Services/IS3FileStorage.cs | 23 ++++ .../Services/S3FileStorage.cs | 40 ++++++- CompanyEmployee.IntegrationTests/BasicTest.cs | 102 ++++++++++++++++-- 6 files changed, 187 insertions(+), 21 deletions(-) diff --git a/CompanyEmployee.Api/Controllers/EmployeeController.cs b/CompanyEmployee.Api/Controllers/EmployeeController.cs index 365d9101..baa925d2 100644 --- a/CompanyEmployee.Api/Controllers/EmployeeController.cs +++ b/CompanyEmployee.Api/Controllers/EmployeeController.cs @@ -26,7 +26,7 @@ public class EmployeeController( [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetEmployee(int id, CancellationToken cancellationToken) + public async Task> GetEmployee([FromQuery] int id, CancellationToken cancellationToken) { try { diff --git a/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs b/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs index 03af09a2..6aa148de 100644 --- a/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs +++ b/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs @@ -1,6 +1,6 @@ -using MassTransit; -using CompanyEmployee.Domain.Entity; +using CompanyEmployee.Domain.Entity; using CompanyEmployee.FileService.Services; +using MassTransit; using System.Text.Json; namespace CompanyEmployee.FileService.Consumers; @@ -14,20 +14,18 @@ public class EmployeeGeneratedConsumer : IConsumer private readonly string _bucketName; private readonly ILogger _logger; - public EmployeeGeneratedConsumer( - IS3FileStorage storage, - string bucketName, - ILogger logger) + public EmployeeGeneratedConsumer(IS3FileStorage storage, string bucketName, ILogger logger) { _storage = storage; _bucketName = bucketName; _logger = logger; } + /// public async Task Consume(ConsumeContext context) { var employee = context.Message; - var fileName = $"employee_{employee.Id}_{DateTime.Now:yyyyMMddHHmmss}.json"; + var fileName = $"employee_{employee.Id}.json"; var content = JsonSerializer.SerializeToUtf8Bytes(employee); await _storage.UploadFileAsync(_bucketName, fileName, content); diff --git a/CompanyEmployee.FileService/Program.cs b/CompanyEmployee.FileService/Program.cs index a74cbb12..3e5aca41 100644 --- a/CompanyEmployee.FileService/Program.cs +++ b/CompanyEmployee.FileService/Program.cs @@ -1,10 +1,10 @@ using Amazon.S3; +using Amazon.SimpleNotificationService; +using Amazon.SQS; using CompanyEmployee.FileService.Consumers; using CompanyEmployee.FileService.Services; using CompanyEmployee.ServiceDefaults; using MassTransit; -using Amazon.SimpleNotificationService; -using Amazon.SQS; var builder = WebApplication.CreateBuilder(args); @@ -41,13 +41,36 @@ h.SecretKey(awsSecretKey); }); - cfg.ConfigureEndpoints(context); + cfg.ReceiveEndpoint("employee-events", e => + { + e.ConfigureConsumer(context); + }); }); }); var app = builder.Build(); +using (var scope = app.Services.CreateScope()) +{ + var storage = scope.ServiceProvider.GetRequiredService(); + await storage.EnsureBucketExistsAsync(bucketName); +} + app.MapDefaultEndpoints(); app.MapGet("/health", () => "Healthy"); +app.MapGet("/api/files/exists/{fileName}", async (string fileName, IS3FileStorage storage, IConfiguration config) => +{ + try + { + var bucket = config["S3:BucketName"] ?? "employee-data"; + var exists = await storage.FileExistsAsync(bucket, fileName); + return exists ? Results.Ok() : Results.NotFound(); + } + catch + { + return Results.NotFound(); + } +}); + app.Run(); \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/IS3FileStorage.cs b/CompanyEmployee.FileService/Services/IS3FileStorage.cs index d090cd4d..48bebd8e 100644 --- a/CompanyEmployee.FileService/Services/IS3FileStorage.cs +++ b/CompanyEmployee.FileService/Services/IS3FileStorage.cs @@ -1,6 +1,29 @@ namespace CompanyEmployee.FileService.Services; +/// +/// Интерфейс для работы с S3 хранилищем. +/// public interface IS3FileStorage { + /// + /// Проверяет существование бакета и создаёт его при необходимости. + /// + /// Имя бакета. + public Task EnsureBucketExistsAsync(string bucketName); + + /// + /// Загружает файл в хранилище. + /// + /// Имя бакета. + /// Ключ (имя файла). + /// Содержимое файла. public Task UploadFileAsync(string bucketName, string key, byte[] content); + + /// + /// Проверяет существование файла в хранилище. + /// + /// Имя бакета. + /// Ключ (имя файла). + /// True если файл существует, иначе False. + public Task FileExistsAsync(string bucketName, string key); } \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/S3FileStorage.cs b/CompanyEmployee.FileService/Services/S3FileStorage.cs index 65f9ee39..cd74326f 100644 --- a/CompanyEmployee.FileService/Services/S3FileStorage.cs +++ b/CompanyEmployee.FileService/Services/S3FileStorage.cs @@ -3,6 +3,9 @@ namespace CompanyEmployee.FileService.Services; +/// +/// Реализация S3 хранилища. +/// public class S3FileStorage : IS3FileStorage { private readonly IAmazonS3 _s3Client; @@ -14,7 +17,8 @@ public S3FileStorage(IAmazonS3 s3Client, ILogger logger) _logger = logger; } - public async Task UploadFileAsync(string bucketName, string key, byte[] content) + /// + public async Task EnsureBucketExistsAsync(string bucketName) { try { @@ -22,7 +26,22 @@ public async Task UploadFileAsync(string bucketName, string key, byte[] content) if (!bucketExists) { await _s3Client.PutBucketAsync(new PutBucketRequest { BucketName = bucketName }); + _logger.LogInformation("Создан бакет {BucketName}", bucketName); } + } + catch (Exception ex) + { + _logger.LogError(ex, "Ошибка при создании бакета {BucketName}", bucketName); + throw; + } + } + + /// + public async Task UploadFileAsync(string bucketName, string key, byte[] content) + { + try + { + await EnsureBucketExistsAsync(bucketName); using var stream = new MemoryStream(content); await _s3Client.PutObjectAsync(new PutObjectRequest @@ -41,4 +60,23 @@ await _s3Client.PutObjectAsync(new PutObjectRequest throw; } } + + /// + public async Task FileExistsAsync(string bucketName, string key) + { + try + { + await _s3Client.GetObjectMetadataAsync(bucketName, key); + return true; + } + catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "Ошибка при проверке файла {Key}", key); + return false; + } + } } \ No newline at end of file diff --git a/CompanyEmployee.IntegrationTests/BasicTest.cs b/CompanyEmployee.IntegrationTests/BasicTest.cs index f918ecc7..e66bb8a2 100644 --- a/CompanyEmployee.IntegrationTests/BasicTest.cs +++ b/CompanyEmployee.IntegrationTests/BasicTest.cs @@ -4,18 +4,30 @@ namespace CompanyEmployee.IntegrationTests; /// -/// . +/// . /// public class BasicTests { - private readonly HttpClient _client; + private readonly HttpClient _apiClient; + private readonly HttpClient _gatewayClient; + private readonly HttpClient _fileServiceClient; public BasicTests() { - _client = new HttpClient + _apiClient = new HttpClient { BaseAddress = new Uri("https://localhost:6001") }; + + _gatewayClient = new HttpClient + { + BaseAddress = new Uri("https://localhost:7000") + }; + + _fileServiceClient = new HttpClient + { + BaseAddress = new Uri("https://localhost:7277") + }; } /// @@ -24,7 +36,7 @@ public BasicTests() [Fact] public async Task GetEmployee_ShouldReturnEmployee() { - var response = await _client.GetAsync("/api/employee/1"); + var response = await _apiClient.GetAsync("/api/employee?id=1"); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); @@ -39,15 +51,15 @@ public async Task GetEmployee_ShouldReturnEmployee() } /// - /// . + /// Redis. /// [Fact] public async Task SameEmployeeId_ShouldReturnSameData() { - var response1 = await _client.GetAsync("/api/employee/2"); + var response1 = await _apiClient.GetAsync("/api/employee?id=2"); var content1 = await response1.Content.ReadAsStringAsync(); - var response2 = await _client.GetAsync("/api/employee/2"); + var response2 = await _apiClient.GetAsync("/api/employee?id=2"); var content2 = await response2.Content.ReadAsStringAsync(); Assert.Equal(content1, content2); @@ -59,12 +71,12 @@ public async Task SameEmployeeId_ShouldReturnSameData() [Fact] public async Task DifferentIds_ShouldGenerateDifferentEmployees() { - var response1 = await _client.GetAsync("/api/employee/10"); + var response1 = await _apiClient.GetAsync("/api/employee?id=10"); var employee1 = JsonSerializer.Deserialize( await response1.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - var response2 = await _client.GetAsync("/api/employee/20"); + var response2 = await _apiClient.GetAsync("/api/employee?id=20"); var employee2 = JsonSerializer.Deserialize( await response2.Content.ReadAsStringAsync(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); @@ -73,4 +85,76 @@ await response2.Content.ReadAsStringAsync(), Assert.NotNull(employee2); Assert.NotEqual(employee1.FullName, employee2.FullName); } + + /// + /// Gateway (Weighted Random). + /// + [Fact] + public async Task LoadBalancer_ShouldDistributeRequests() + { + var requestsCount = 50; + var successCount = 0; + + for (var i = 0; i < requestsCount; i++) + { + var response = await _gatewayClient.GetAsync($"/api/employee?id={i + 1}"); + if (response.IsSuccessStatusCode) + { + successCount++; + } + } + + Assert.Equal(requestsCount, successCount); + } + + /// + /// SNS S3. + /// + [Fact] + public async Task EmployeeGeneration_ShouldSendToSnsAndSaveToS3() + { + var employeeId = 777; + await _fileServiceClient.GetAsync("/health"); + await Task.Delay(5000); + var response = await _apiClient.GetAsync($"/api/employee?id={employeeId}"); + response.EnsureSuccessStatusCode(); + + await Task.Delay(3000); + + var fileCheckResponse = await _fileServiceClient.GetAsync($"/api/files/exists/employee_{employeeId}.json"); + + Assert.True(fileCheckResponse.IsSuccessStatusCode, " S3 "); + } + + /// + /// : -> -> -> . + /// + [Fact] + public async Task FullCycle_ShouldWorkCorrectly() + { + var employeeId = 777; + + var startTime = DateTime.Now; + var response = await _apiClient.GetAsync($"/api/employee?id={employeeId}"); + var firstDuration = DateTime.Now - startTime; + + response.EnsureSuccessStatusCode(); + var employee = JsonSerializer.Deserialize( + await response.Content.ReadAsStringAsync(), + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + Assert.NotNull(employee); + Assert.Equal(employeeId, employee.Id); + + startTime = DateTime.Now; + var cachedResponse = await _apiClient.GetAsync($"/api/employee?id={employeeId}"); + var secondDuration = DateTime.Now - startTime; + + Assert.True(secondDuration < firstDuration, " ()"); + + await Task.Delay(3000); + + var fileExists = await _fileServiceClient.GetAsync($"/api/files/exists/employee_{employeeId}.json"); + Assert.True(fileExists.IsSuccessStatusCode, " S3"); + } } \ No newline at end of file From 4c9a32e4081e8036d100eb274c259c924a9c3fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Sat, 4 Apr 2026 01:23:13 +0400 Subject: [PATCH 22/24] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вроде, правильно должно быть Еще раз прошу не осуждать, я пытаюсь развиваться в этом предмете, правда... --- Client.Wasm/Components/StudentCard.razor | 4 +- .../Services/EmployeeService.cs | 3 + CompanyEmployee.AppHost/AppHost.cs | 48 ++++++----- .../CompanyEmployee.FileService.csproj | 4 +- .../Consumers/EmployeeGeneratedConsumer.cs | 9 +- CompanyEmployee.FileService/Program.cs | 58 +++++-------- .../Services/IS3FileStorage.cs | 29 ------- .../Services/IStorageService.cs | 17 ++++ .../Services/MinioStorageService.cs | 62 ++++++++++++++ .../Services/S3FileStorage.cs | 82 ------------------- 10 files changed, 138 insertions(+), 178 deletions(-) delete mode 100644 CompanyEmployee.FileService/Services/IS3FileStorage.cs create mode 100644 CompanyEmployee.FileService/Services/IStorageService.cs create mode 100644 CompanyEmployee.FileService/Services/MinioStorageService.cs delete mode 100644 CompanyEmployee.FileService/Services/S3FileStorage.cs diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor index 0b487b1b..a470fb50 100644 --- a/Client.Wasm/Components/StudentCard.razor +++ b/Client.Wasm/Components/StudentCard.razor @@ -4,8 +4,8 @@ - Номер №2 "Балансировка нагрузки" - Вариант №43 "Weighted Random" + Номер №3 "Интеграционное тестирование" + Вариант №43 "Сотрудник компании" Выполнена Казаковым Андреем 6513 Ссылка на форк diff --git a/CompanyEmployee.Api/Services/EmployeeService.cs b/CompanyEmployee.Api/Services/EmployeeService.cs index 61a6a906..56380a0c 100644 --- a/CompanyEmployee.Api/Services/EmployeeService.cs +++ b/CompanyEmployee.Api/Services/EmployeeService.cs @@ -5,6 +5,9 @@ namespace CompanyEmployee.Api.Services; +/// +/// Сервис для работы с сотрудниками. +/// public class EmployeeService : IEmployeeService { private readonly IEmployeeGenerator _generator; diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index c8573a42..6fe2abab 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -5,26 +5,20 @@ var redis = builder.AddRedis("redis") .WithRedisCommander(); -var authToken = builder.Configuration["LocalStack:AuthToken"]; - -var localstack = builder.AddContainer("localstack", "localstack/localstack") +var localstack = builder.AddContainer("localstack", "localstack/localstack:3.0") .WithEndpoint(port: 4566, targetPort: 4566, name: "localstack", scheme: "http") - .WithEnvironment("SERVICES", "sns,sqs,s3") - .WithEnvironment("DEFAULT_REGION", "us-east-1") - .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1"); + .WithEnvironment("SERVICES", "sns,sqs") + .WithEnvironment("AWS_ACCESS_KEY_ID", "test") + .WithEnvironment("AWS_SECRET_ACCESS_KEY", "test") + .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1") + .WithLifetime(ContainerLifetime.Persistent); -if (!string.IsNullOrEmpty(authToken)) -{ - localstack = localstack.WithEnvironment("LOCALSTACK_AUTH_TOKEN", authToken); -} - -var fileService = builder.AddProject("fileservice") - .WaitFor(localstack) - .WithEnvironment("AWS__ServiceURL", "http://localhost:4566") - .WithEnvironment("AWS__Region", "us-east-1") - .WithEnvironment("AWS__AccessKeyId", "test") - .WithEnvironment("AWS__SecretAccessKey", "test") - .WithEnvironment("S3__BucketName", "employee-data"); +var minio = builder.AddContainer("minio", "minio/minio") + .WithArgs("server", "/data", "--console-address", ":9001") + .WithEnvironment("MINIO_ROOT_USER", "minioadmin") + .WithEnvironment("MINIO_ROOT_PASSWORD", "minioadmin") + .WithEndpoint(port: 9000, targetPort: 9000, name: "api", scheme: "http") + .WithEndpoint(port: 9001, targetPort: 9001, name: "console", scheme: "http"); var gateway = builder.AddProject("gateway") .WithEndpoint("https", e => e.Port = 7000) @@ -39,21 +33,31 @@ var port = startApiPort + i; var api = builder.AddProject($"api-{i + 1}") .WithReference(redis) - .WaitFor(redis) - .WaitFor(localstack) - .WaitFor(fileService) .WithEndpoint("https", e => e.Port = port) .WithEnvironment("ASPNETCORE_ENVIRONMENT", "Development") .WithEnvironment("AWS__ServiceURL", "http://localhost:4566") .WithEnvironment("AWS__Region", "us-east-1") .WithEnvironment("AWS__AccessKeyId", "test") .WithEnvironment("AWS__SecretAccessKey", "test") - .WithEnvironment("SNS__TopicArn", "arn:aws:sns:us-east-1:000000000000:employee-events"); + .WaitFor(redis) + .WaitFor(localstack); apiReplicas.Add(api); gateway.WaitFor(api); } +var fileService = builder.AddProject("fileservice") + .WaitFor(localstack) + .WaitFor(minio) + .WithEnvironment("AWS__ServiceURL", "http://localhost:4566") + .WithEnvironment("AWS__Region", "us-east-1") + .WithEnvironment("AWS__AccessKeyId", "test") + .WithEnvironment("AWS__SecretAccessKey", "test") + .WithEnvironment("MINIO__Endpoint", "localhost:9000") + .WithEnvironment("MINIO__AccessKey", "minioadmin") + .WithEnvironment("MINIO__SecretKey", "minioadmin") + .WithEnvironment("MINIO__BucketName", "employee-data"); + foreach (var replica in apiReplicas) { gateway.WithReference(replica); diff --git a/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj b/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj index 719999de..76d705f1 100644 --- a/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj +++ b/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -7,8 +7,8 @@ - + diff --git a/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs b/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs index 6aa148de..8b084bd9 100644 --- a/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs +++ b/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs @@ -10,25 +10,24 @@ namespace CompanyEmployee.FileService.Consumers; /// public class EmployeeGeneratedConsumer : IConsumer { - private readonly IS3FileStorage _storage; + private readonly IStorageService _storage; private readonly string _bucketName; private readonly ILogger _logger; - public EmployeeGeneratedConsumer(IS3FileStorage storage, string bucketName, ILogger logger) + public EmployeeGeneratedConsumer(IStorageService storage, string bucketName, ILogger logger) { _storage = storage; _bucketName = bucketName; _logger = logger; } - /// public async Task Consume(ConsumeContext context) { var employee = context.Message; var fileName = $"employee_{employee.Id}.json"; var content = JsonSerializer.SerializeToUtf8Bytes(employee); - await _storage.UploadFileAsync(_bucketName, fileName, content); - _logger.LogInformation("Сотрудник {Id} сохранён в S3 как {FileName}", employee.Id, fileName); + await _storage.SaveFileAsync(_bucketName, fileName, content); + _logger.LogInformation("Сотрудник {Id} сохранён в MinIO как {FileName}", employee.Id, fileName); } } \ No newline at end of file diff --git a/CompanyEmployee.FileService/Program.cs b/CompanyEmployee.FileService/Program.cs index 3e5aca41..535723f2 100644 --- a/CompanyEmployee.FileService/Program.cs +++ b/CompanyEmployee.FileService/Program.cs @@ -1,31 +1,34 @@ -using Amazon.S3; -using Amazon.SimpleNotificationService; -using Amazon.SQS; using CompanyEmployee.FileService.Consumers; using CompanyEmployee.FileService.Services; using CompanyEmployee.ServiceDefaults; using MassTransit; +using Minio; +using Amazon.SimpleNotificationService; +using Amazon.SQS; var builder = WebApplication.CreateBuilder(args); builder.AddServiceDefaults(); +var minioEndpoint = builder.Configuration["MinIO:Endpoint"] ?? "localhost:9000"; +var minioAccessKey = builder.Configuration["MinIO:AccessKey"] ?? "minioadmin"; +var minioSecretKey = builder.Configuration["MinIO:SecretKey"] ?? "minioadmin"; +var bucketName = builder.Configuration["MinIO:BucketName"] ?? "employee-data"; + +builder.Services.AddSingleton(new MinioClient() + .WithEndpoint(minioEndpoint) + .WithCredentials(minioAccessKey, minioSecretKey) + .WithSSL(false) + .Build()); + +// IStorageService MinioStorageService +builder.Services.AddSingleton(); +builder.Services.AddSingleton(bucketName); + var awsServiceUrl = builder.Configuration["AWS:ServiceURL"] ?? "http://localhost:4566"; var awsRegion = builder.Configuration["AWS:Region"] ?? "us-east-1"; var awsAccessKey = builder.Configuration["AWS:AccessKeyId"] ?? "test"; var awsSecretKey = builder.Configuration["AWS:SecretAccessKey"] ?? "test"; -var bucketName = builder.Configuration["S3:BucketName"] ?? "employee-data"; - -builder.Services.AddSingleton(_ => new AmazonS3Client( - awsAccessKey, awsSecretKey, - new AmazonS3Config - { - ServiceURL = awsServiceUrl, - ForcePathStyle = true - })); - -builder.Services.AddSingleton(); -builder.Services.AddSingleton(bucketName); builder.Services.AddMassTransit(x => { @@ -41,36 +44,19 @@ h.SecretKey(awsSecretKey); }); - cfg.ReceiveEndpoint("employee-events", e => - { - e.ConfigureConsumer(context); - }); + cfg.ConfigureEndpoints(context); }); }); var app = builder.Build(); -using (var scope = app.Services.CreateScope()) -{ - var storage = scope.ServiceProvider.GetRequiredService(); - await storage.EnsureBucketExistsAsync(bucketName); -} - app.MapDefaultEndpoints(); app.MapGet("/health", () => "Healthy"); -app.MapGet("/api/files/exists/{fileName}", async (string fileName, IS3FileStorage storage, IConfiguration config) => +app.MapGet("/api/files/exists/{fileName}", async (string fileName, IStorageService storage) => { - try - { - var bucket = config["S3:BucketName"] ?? "employee-data"; - var exists = await storage.FileExistsAsync(bucket, fileName); - return exists ? Results.Ok() : Results.NotFound(); - } - catch - { - return Results.NotFound(); - } + var exists = await storage.FileExistsAsync(bucketName, fileName); + return exists ? Results.Ok() : Results.NotFound(); }); app.Run(); \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/IS3FileStorage.cs b/CompanyEmployee.FileService/Services/IS3FileStorage.cs deleted file mode 100644 index 48bebd8e..00000000 --- a/CompanyEmployee.FileService/Services/IS3FileStorage.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace CompanyEmployee.FileService.Services; - -/// -/// Интерфейс для работы с S3 хранилищем. -/// -public interface IS3FileStorage -{ - /// - /// Проверяет существование бакета и создаёт его при необходимости. - /// - /// Имя бакета. - public Task EnsureBucketExistsAsync(string bucketName); - - /// - /// Загружает файл в хранилище. - /// - /// Имя бакета. - /// Ключ (имя файла). - /// Содержимое файла. - public Task UploadFileAsync(string bucketName, string key, byte[] content); - - /// - /// Проверяет существование файла в хранилище. - /// - /// Имя бакета. - /// Ключ (имя файла). - /// True если файл существует, иначе False. - public Task FileExistsAsync(string bucketName, string key); -} \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/IStorageService.cs b/CompanyEmployee.FileService/Services/IStorageService.cs new file mode 100644 index 00000000..0fba6522 --- /dev/null +++ b/CompanyEmployee.FileService/Services/IStorageService.cs @@ -0,0 +1,17 @@ +namespace CompanyEmployee.FileService.Services; + +/// +/// Интерфейс для работы с объектным хранилищем. +/// +public interface IStorageService +{ + /// + /// Сохраняет файл в хранилище. + /// + public Task SaveFileAsync(string bucketName, string key, byte[] content); + + /// + /// Проверяет существование файла в хранилище. + /// + public Task FileExistsAsync(string bucketName, string key); +} \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/MinioStorageService.cs b/CompanyEmployee.FileService/Services/MinioStorageService.cs new file mode 100644 index 00000000..84c7c9f1 --- /dev/null +++ b/CompanyEmployee.FileService/Services/MinioStorageService.cs @@ -0,0 +1,62 @@ +using Minio; +using Minio.DataModel.Args; + +namespace CompanyEmployee.FileService.Services; + +/// +/// Реализация хранилища через MinIO. +/// +public class MinioStorageService : IStorageService +{ + private readonly IMinioClient _minioClient; + private readonly ILogger _logger; + + public MinioStorageService(IMinioClient minioClient, ILogger logger) + { + _minioClient = minioClient; + _logger = logger; + } + + public async Task SaveFileAsync(string bucketName, string key, byte[] content) + { + try + { + var bucketExists = await _minioClient.BucketExistsAsync(new BucketExistsArgs().WithBucket(bucketName)); + if (!bucketExists) + { + await _minioClient.MakeBucketAsync(new MakeBucketArgs().WithBucket(bucketName)); + _logger.LogInformation("Создан бакет {BucketName}", bucketName); + } + + using var stream = new MemoryStream(content); + await _minioClient.PutObjectAsync(new PutObjectArgs() + .WithBucket(bucketName) + .WithObject(key) + .WithStreamData(stream) + .WithObjectSize(stream.Length) + .WithContentType("application/json")); + + _logger.LogInformation("Файл {Key} загружен в MinIO", key); + } + catch (Exception ex) + { + _logger.LogError(ex, "Ошибка загрузки файла {Key}", key); + throw; + } + } + + public async Task FileExistsAsync(string bucketName, string key) + { + try + { + await _minioClient.StatObjectAsync(new StatObjectArgs() + .WithBucket(bucketName) + .WithObject(key)); + return true; + } + catch + { + return false; + } + } +} \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/S3FileStorage.cs b/CompanyEmployee.FileService/Services/S3FileStorage.cs deleted file mode 100644 index cd74326f..00000000 --- a/CompanyEmployee.FileService/Services/S3FileStorage.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Amazon.S3; -using Amazon.S3.Model; - -namespace CompanyEmployee.FileService.Services; - -/// -/// Реализация S3 хранилища. -/// -public class S3FileStorage : IS3FileStorage -{ - private readonly IAmazonS3 _s3Client; - private readonly ILogger _logger; - - public S3FileStorage(IAmazonS3 s3Client, ILogger logger) - { - _s3Client = s3Client; - _logger = logger; - } - - /// - public async Task EnsureBucketExistsAsync(string bucketName) - { - try - { - var bucketExists = await _s3Client.DoesS3BucketExistAsync(bucketName); - if (!bucketExists) - { - await _s3Client.PutBucketAsync(new PutBucketRequest { BucketName = bucketName }); - _logger.LogInformation("Создан бакет {BucketName}", bucketName); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Ошибка при создании бакета {BucketName}", bucketName); - throw; - } - } - - /// - public async Task UploadFileAsync(string bucketName, string key, byte[] content) - { - try - { - await EnsureBucketExistsAsync(bucketName); - - using var stream = new MemoryStream(content); - await _s3Client.PutObjectAsync(new PutObjectRequest - { - BucketName = bucketName, - Key = key, - InputStream = stream, - ContentType = "application/json" - }); - - _logger.LogInformation("Файл {Key} загружен в {BucketName}", key, bucketName); - } - catch (Exception ex) - { - _logger.LogError(ex, "Ошибка загрузки файла {Key}", key); - throw; - } - } - - /// - public async Task FileExistsAsync(string bucketName, string key) - { - try - { - await _s3Client.GetObjectMetadataAsync(bucketName, key); - return true; - } - catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound) - { - return false; - } - catch (Exception ex) - { - _logger.LogError(ex, "Ошибка при проверке файла {Key}", key); - return false; - } - } -} \ No newline at end of file From 43b081d526b09bf156babcc5bcbd99436d2db08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Sat, 4 Apr 2026 11:55:48 +0400 Subject: [PATCH 23/24] =?UTF-8?q?=D0=BC=D0=B8=D0=BD=D0=B8=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/EmployeeService.cs | 56 ++++++++----------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/CompanyEmployee.Api/Services/EmployeeService.cs b/CompanyEmployee.Api/Services/EmployeeService.cs index 56380a0c..db244a34 100644 --- a/CompanyEmployee.Api/Services/EmployeeService.cs +++ b/CompanyEmployee.Api/Services/EmployeeService.cs @@ -8,30 +8,22 @@ namespace CompanyEmployee.Api.Services; /// /// Сервис для работы с сотрудниками. /// -public class EmployeeService : IEmployeeService +/// Генератор сотрудников. +/// Кэш Redis. +/// Endpoint для отправки сообщений в SNS. +/// Логгер. +public class EmployeeService( + IEmployeeGenerator generator, + IDistributedCache cache, + IPublishEndpoint publishEndpoint, + ILogger logger) : IEmployeeService { - private readonly IEmployeeGenerator _generator; - private readonly IDistributedCache _cache; - private readonly IPublishEndpoint _publishEndpoint; - private readonly ILogger _logger; - private readonly DistributedCacheEntryOptions _cacheOptions; - - public EmployeeService( - IEmployeeGenerator generator, - IDistributedCache cache, - IPublishEndpoint publishEndpoint, - ILogger logger) + private readonly DistributedCacheEntryOptions _cacheOptions = new() { - _generator = generator; - _cache = cache; - _publishEndpoint = publishEndpoint; - _logger = logger; - _cacheOptions = new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) - }; - } + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) + }; + /// public async Task GetEmployeeAsync(int id, CancellationToken cancellationToken = default) { var cacheKey = $"employee:{id}"; @@ -39,42 +31,42 @@ public EmployeeService( try { - var cachedJson = await _cache.GetStringAsync(cacheKey, cancellationToken); + var cachedJson = await cache.GetStringAsync(cacheKey, cancellationToken); if (cachedJson != null) { - _logger.LogInformation("Сотрудник с ID {Id} найден в кэше", id); + logger.LogInformation("Сотрудник с ID {Id} найден в кэше", id); employee = JsonSerializer.Deserialize(cachedJson); return employee; } } catch (Exception ex) { - _logger.LogError(ex, "Ошибка при получении данных из кэша"); + logger.LogError(ex, "Ошибка при получении данных из кэша"); } try { - _logger.LogInformation("Сотрудник с ID {Id} не найден в кэше, генерация нового", id); - employee = _generator.Generate(id); + logger.LogInformation("Сотрудник с ID {Id} не найден в кэше, генерация нового", id); + employee = generator.Generate(id); } catch (Exception ex) { - _logger.LogError(ex, "Ошибка при генерации сотрудника с ID {Id}", id); + logger.LogError(ex, "Ошибка при генерации сотрудника с ID {Id}", id); return null; } try { var serialized = JsonSerializer.Serialize(employee); - await _cache.SetStringAsync(cacheKey, serialized, _cacheOptions, cancellationToken); - _logger.LogDebug("Сотрудник с ID {Id} сохранён в кэш", id); + await cache.SetStringAsync(cacheKey, serialized, _cacheOptions, cancellationToken); + logger.LogDebug("Сотрудник с ID {Id} сохранён в кэш", id); - await _publishEndpoint.Publish(employee, cancellationToken); - _logger.LogInformation("Сообщение о сотруднике {Id} отправлено в SNS", id); + await publishEndpoint.Publish(employee, cancellationToken); + logger.LogInformation("Сообщение о сотруднике {Id} отправлено в SNS", id); } catch (Exception ex) { - _logger.LogWarning(ex, "Не удалось отправить сообщение о сотруднике {Id}", id); + logger.LogWarning(ex, "Не удалось отправить сообщение о сотруднике {Id}", id); } return employee; From faabc52554e058492da7dcc207363f5c751c3400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D0=B4=D1=80=D0=B5=D0=B9?= Date: Thu, 9 Apr 2026 14:30:05 +0400 Subject: [PATCH 24/24] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пока не готово, но уже что-то --- .../CompanyEmployee.Api.csproj | 4 +- CompanyEmployee.Api/Program.cs | 26 ++-- .../Services/EmployeeService.cs | 11 +- .../Services/SnsPublisherService.cs | 71 +++++++++++ CompanyEmployee.Api/appsettings.json | 1 - CompanyEmployee.AppHost/AppHost.cs | 30 ++--- .../CompanyEmployee.AppHost.csproj | 1 + CompanyEmployee.AppHost/setup-sns.ps1 | 32 +++++ .../CompanyEmployee.FileService.csproj | 4 +- .../Consumers/EmployeeGeneratedConsumer.cs | 33 ----- CompanyEmployee.FileService/Program.cs | 117 +++++++++++++----- .../Properties/launchSettings.json | 2 +- .../Services/IStorageService.cs | 24 +++- .../Services/MinioStorageService.cs | 94 +++++++++++--- CompanyEmployee.FileService/appsettings.json | 11 +- 15 files changed, 319 insertions(+), 142 deletions(-) create mode 100644 CompanyEmployee.Api/Services/SnsPublisherService.cs create mode 100644 CompanyEmployee.AppHost/setup-sns.ps1 delete mode 100644 CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs diff --git a/CompanyEmployee.Api/CompanyEmployee.Api.csproj b/CompanyEmployee.Api/CompanyEmployee.Api.csproj index 6210ba34..93740694 100644 --- a/CompanyEmployee.Api/CompanyEmployee.Api.csproj +++ b/CompanyEmployee.Api/CompanyEmployee.Api.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,8 +8,8 @@ + - diff --git a/CompanyEmployee.Api/Program.cs b/CompanyEmployee.Api/Program.cs index 32df462c..fb110e9c 100644 --- a/CompanyEmployee.Api/Program.cs +++ b/CompanyEmployee.Api/Program.cs @@ -1,8 +1,6 @@ using Amazon.SimpleNotificationService; -using Amazon.SQS; using CompanyEmployee.Api.Services; using CompanyEmployee.ServiceDefaults; -using MassTransit; var builder = WebApplication.CreateBuilder(args); @@ -18,21 +16,17 @@ var awsAccessKey = builder.Configuration["AWS:AccessKeyId"] ?? "test"; var awsSecretKey = builder.Configuration["AWS:SecretAccessKey"] ?? "test"; -builder.Services.AddMassTransit(x => +var snsConfig = new AmazonSimpleNotificationServiceConfig { - x.UsingAmazonSqs((context, cfg) => - { - cfg.Host(awsRegion, h => - { - h.Config(new AmazonSQSConfig { ServiceURL = awsServiceUrl }); - h.Config(new AmazonSimpleNotificationServiceConfig { ServiceURL = awsServiceUrl }); - h.AccessKey(awsAccessKey); - h.SecretKey(awsSecretKey); - }); - }); -}); - -builder.Services.AddSingleton(sp => sp.GetRequiredService()); + ServiceURL = awsServiceUrl, + AuthenticationRegion = awsRegion, + UseHttp = true +}; + +builder.Services.AddSingleton(_ => + new AmazonSimpleNotificationServiceClient(awsAccessKey, awsSecretKey, snsConfig)); + +builder.Services.AddSingleton(); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); diff --git a/CompanyEmployee.Api/Services/EmployeeService.cs b/CompanyEmployee.Api/Services/EmployeeService.cs index db244a34..87b8331d 100644 --- a/CompanyEmployee.Api/Services/EmployeeService.cs +++ b/CompanyEmployee.Api/Services/EmployeeService.cs @@ -1,7 +1,6 @@ using System.Text.Json; using CompanyEmployee.Domain.Entity; using Microsoft.Extensions.Caching.Distributed; -using MassTransit; namespace CompanyEmployee.Api.Services; @@ -10,12 +9,12 @@ namespace CompanyEmployee.Api.Services; /// /// Генератор сотрудников. /// Кэш Redis. -/// Endpoint для отправки сообщений в SNS. +/// Публикатор SNS. /// Логгер. public class EmployeeService( IEmployeeGenerator generator, IDistributedCache cache, - IPublishEndpoint publishEndpoint, + SnsPublisherService snsPublisher, ILogger logger) : IEmployeeService { private readonly DistributedCacheEntryOptions _cacheOptions = new() @@ -23,7 +22,6 @@ public class EmployeeService( AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }; - /// public async Task GetEmployeeAsync(int id, CancellationToken cancellationToken = default) { var cacheKey = $"employee:{id}"; @@ -61,12 +59,11 @@ public class EmployeeService( await cache.SetStringAsync(cacheKey, serialized, _cacheOptions, cancellationToken); logger.LogDebug("Сотрудник с ID {Id} сохранён в кэш", id); - await publishEndpoint.Publish(employee, cancellationToken); - logger.LogInformation("Сообщение о сотруднике {Id} отправлено в SNS", id); + await snsPublisher.PublishEmployeeAsync(employee, cancellationToken); } catch (Exception ex) { - logger.LogWarning(ex, "Не удалось отправить сообщение о сотруднике {Id}", id); + logger.LogWarning(ex, "Не удалось отправить сотрудника {Id} в SNS", id); } return employee; diff --git a/CompanyEmployee.Api/Services/SnsPublisherService.cs b/CompanyEmployee.Api/Services/SnsPublisherService.cs new file mode 100644 index 00000000..6cdc1b03 --- /dev/null +++ b/CompanyEmployee.Api/Services/SnsPublisherService.cs @@ -0,0 +1,71 @@ +using Amazon.SimpleNotificationService; +using Amazon.SimpleNotificationService.Model; +using CompanyEmployee.Domain.Entity; +using System.Text.Json; + +namespace CompanyEmployee.Api.Services; + +/// +/// Сервис для публикации сообщений в SNS. +/// +/// SNS клиент. +/// Конфигурация. +/// Логгер. +public class SnsPublisherService( + IAmazonSimpleNotificationService snsClient, + IConfiguration configuration, + ILogger logger) +{ + private readonly string? _topicArn = configuration["SNS:TopicArn"] ?? "arn:aws:sns:us-east-1:000000000000:employee-events"; + + public async Task PublishEmployeeAsync(Employee employee, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(_topicArn)) + { + logger.LogWarning("SNS TopicArn не настроен, публикация пропущена"); + return; + } + + try + { + var message = JsonSerializer.Serialize(employee); + var publishRequest = new PublishRequest + { + TopicArn = _topicArn, + Message = message, + Subject = $"Employee-{employee.Id}" + }; + var response = await snsClient.PublishAsync(publishRequest, cancellationToken); + logger.LogInformation("Сотрудник {Id} опубликован в SNS, MessageId: {MessageId}", employee.Id, response.MessageId); + } + catch (NotFoundException) + { + logger.LogWarning("Топик SNS не существует, попытка создать"); + try + { + var createTopicRequest = new CreateTopicRequest { Name = "employee-events" }; + var createResponse = await snsClient.CreateTopicAsync(createTopicRequest, cancellationToken); + var createdTopicArn = createResponse.TopicArn; + logger.LogInformation("Топик SNS создан: {TopicArn}", createdTopicArn); + + var message = JsonSerializer.Serialize(employee); + var publishRequest = new PublishRequest + { + TopicArn = createdTopicArn, + Message = message, + Subject = $"Employee-{employee.Id}" + }; + var response = await snsClient.PublishAsync(publishRequest, cancellationToken); + logger.LogInformation("Сотрудник {Id} опубликован в SNS после создания топика, MessageId: {MessageId}", employee.Id, response.MessageId); + } + catch (Exception ex) + { + logger.LogError(ex, "Не удалось создать топик SNS и опубликовать сообщение"); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка при публикации в SNS"); + } + } +} \ No newline at end of file diff --git a/CompanyEmployee.Api/appsettings.json b/CompanyEmployee.Api/appsettings.json index 1361d0b6..10f16843 100644 --- a/CompanyEmployee.Api/appsettings.json +++ b/CompanyEmployee.Api/appsettings.json @@ -5,7 +5,6 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*", "AWS": { "ServiceURL": "http://localhost:4566", "Region": "us-east-1", diff --git a/CompanyEmployee.AppHost/AppHost.cs b/CompanyEmployee.AppHost/AppHost.cs index 6fe2abab..77afb095 100644 --- a/CompanyEmployee.AppHost/AppHost.cs +++ b/CompanyEmployee.AppHost/AppHost.cs @@ -5,14 +5,6 @@ var redis = builder.AddRedis("redis") .WithRedisCommander(); -var localstack = builder.AddContainer("localstack", "localstack/localstack:3.0") - .WithEndpoint(port: 4566, targetPort: 4566, name: "localstack", scheme: "http") - .WithEnvironment("SERVICES", "sns,sqs") - .WithEnvironment("AWS_ACCESS_KEY_ID", "test") - .WithEnvironment("AWS_SECRET_ACCESS_KEY", "test") - .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1") - .WithLifetime(ContainerLifetime.Persistent); - var minio = builder.AddContainer("minio", "minio/minio") .WithArgs("server", "/data", "--console-address", ":9001") .WithEnvironment("MINIO_ROOT_USER", "minioadmin") @@ -20,6 +12,13 @@ .WithEndpoint(port: 9000, targetPort: 9000, name: "api", scheme: "http") .WithEndpoint(port: 9001, targetPort: 9001, name: "console", scheme: "http"); +var localstack = builder.AddContainer("localstack", "localstack/localstack:3.8.0") + .WithEndpoint(port: 4566, targetPort: 4566, name: "localstack", scheme: "http") + .WithEnvironment("SERVICES", "sns") + .WithEnvironment("AWS_ACCESS_KEY_ID", "test") + .WithEnvironment("AWS_SECRET_ACCESS_KEY", "test") + .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1"); + var gateway = builder.AddProject("gateway") .WithEndpoint("https", e => e.Port = 7000) .WithExternalHttpEndpoints(); @@ -39,6 +38,7 @@ .WithEnvironment("AWS__Region", "us-east-1") .WithEnvironment("AWS__AccessKeyId", "test") .WithEnvironment("AWS__SecretAccessKey", "test") + .WithEnvironment("SNS__TopicArn", "arn:aws:sns:us-east-1:000000000000:employee-events") .WaitFor(redis) .WaitFor(localstack); @@ -47,16 +47,12 @@ } var fileService = builder.AddProject("fileservice") - .WaitFor(localstack) .WaitFor(minio) - .WithEnvironment("AWS__ServiceURL", "http://localhost:4566") - .WithEnvironment("AWS__Region", "us-east-1") - .WithEnvironment("AWS__AccessKeyId", "test") - .WithEnvironment("AWS__SecretAccessKey", "test") - .WithEnvironment("MINIO__Endpoint", "localhost:9000") - .WithEnvironment("MINIO__AccessKey", "minioadmin") - .WithEnvironment("MINIO__SecretKey", "minioadmin") - .WithEnvironment("MINIO__BucketName", "employee-data"); + .WaitFor(localstack) + .WithEnvironment("MinIO__Endpoint", "http://localhost:9000") + .WithEnvironment("MinIO__AccessKey", "minioadmin") + .WithEnvironment("MinIO__SecretKey", "minioadmin") + .WithEnvironment("MinIO__BucketName", "employee-data"); foreach (var replica in apiReplicas) { diff --git a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj index 2f38984d..6a08073b 100644 --- a/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj +++ b/CompanyEmployee.AppHost/CompanyEmployee.AppHost.csproj @@ -14,6 +14,7 @@ + diff --git a/CompanyEmployee.AppHost/setup-sns.ps1 b/CompanyEmployee.AppHost/setup-sns.ps1 new file mode 100644 index 00000000..b6dfd06a --- /dev/null +++ b/CompanyEmployee.AppHost/setup-sns.ps1 @@ -0,0 +1,32 @@ +Write-Host "Initializing LocalStack for SNS" + +$maxAttempts = 30 +$attempt = 0 +while ($attempt -lt $maxAttempts) { + try { + awslocal sns list-topics 2>$null + if ($LASTEXITCODE -eq 0) { + Write-Host "LocalStack is ready!" + break + } + } catch {} + $attempt++ + Write-Host "Waiting for LocalStack (attempt $attempt/$maxAttempts)" + Start-Sleep -Seconds 1 +} + +if ($attempt -eq $maxAttempts) { + Write-Host "ERROR: LocalStack failed to start" + exit 1 +} + +Write-Host "Creating SNS topic..." +awslocal sns create-topic --name employee-events + +Write-Host "Subscribing FileService to SNS topic..." +awslocal sns subscribe ` + --topic-arn arn:aws:sns:us-east-1:000000000000:employee-events ` + --protocol http ` + --notification-endpoint http://host.docker.internal:7277/api/sns/notification + +Write-Host "SNS topic created and FileService subscribed successfully" \ No newline at end of file diff --git a/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj b/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj index 76d705f1..059f3c75 100644 --- a/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj +++ b/CompanyEmployee.FileService/CompanyEmployee.FileService.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs b/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs deleted file mode 100644 index 8b084bd9..00000000 --- a/CompanyEmployee.FileService/Consumers/EmployeeGeneratedConsumer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using CompanyEmployee.Domain.Entity; -using CompanyEmployee.FileService.Services; -using MassTransit; -using System.Text.Json; - -namespace CompanyEmployee.FileService.Consumers; - -/// -/// Консьюмер для обработки сообщений о генерации сотрудника. -/// -public class EmployeeGeneratedConsumer : IConsumer -{ - private readonly IStorageService _storage; - private readonly string _bucketName; - private readonly ILogger _logger; - - public EmployeeGeneratedConsumer(IStorageService storage, string bucketName, ILogger logger) - { - _storage = storage; - _bucketName = bucketName; - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - var employee = context.Message; - var fileName = $"employee_{employee.Id}.json"; - var content = JsonSerializer.SerializeToUtf8Bytes(employee); - - await _storage.SaveFileAsync(_bucketName, fileName, content); - _logger.LogInformation("Сотрудник {Id} сохранён в MinIO как {FileName}", employee.Id, fileName); - } -} \ No newline at end of file diff --git a/CompanyEmployee.FileService/Program.cs b/CompanyEmployee.FileService/Program.cs index 535723f2..77b49f54 100644 --- a/CompanyEmployee.FileService/Program.cs +++ b/CompanyEmployee.FileService/Program.cs @@ -1,57 +1,100 @@ -using CompanyEmployee.FileService.Consumers; +using CompanyEmployee.Domain.Entity; using CompanyEmployee.FileService.Services; using CompanyEmployee.ServiceDefaults; -using MassTransit; using Minio; -using Amazon.SimpleNotificationService; -using Amazon.SQS; +using Minio.DataModel.Args; +using System.Text; +using System.Text.Json; var builder = WebApplication.CreateBuilder(args); - builder.AddServiceDefaults(); -var minioEndpoint = builder.Configuration["MinIO:Endpoint"] ?? "localhost:9000"; -var minioAccessKey = builder.Configuration["MinIO:AccessKey"] ?? "minioadmin"; -var minioSecretKey = builder.Configuration["MinIO:SecretKey"] ?? "minioadmin"; -var bucketName = builder.Configuration["MinIO:BucketName"] ?? "employee-data"; +builder.AddMinioClient("minio"); -builder.Services.AddSingleton(new MinioClient() - .WithEndpoint(minioEndpoint) - .WithCredentials(minioAccessKey, minioSecretKey) - .WithSSL(false) - .Build()); +var bucketName = builder.Configuration["MinIO:BucketName"] ?? "employee-data"; -// IStorageService MinioStorageService builder.Services.AddSingleton(); builder.Services.AddSingleton(bucketName); -var awsServiceUrl = builder.Configuration["AWS:ServiceURL"] ?? "http://localhost:4566"; -var awsRegion = builder.Configuration["AWS:Region"] ?? "us-east-1"; -var awsAccessKey = builder.Configuration["AWS:AccessKeyId"] ?? "test"; -var awsSecretKey = builder.Configuration["AWS:SecretAccessKey"] ?? "test"; +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); -builder.Services.AddMassTransit(x => +using (var scope = app.Services.CreateScope()) { - x.AddConsumer(); + var minioClient = scope.ServiceProvider.GetRequiredService(); + var bucket = bucketName; + + var bucketExists = await minioClient.BucketExistsAsync(new BucketExistsArgs().WithBucket(bucket)); + if (!bucketExists) + { + await minioClient.MakeBucketAsync(new MakeBucketArgs().WithBucket(bucket)); + Console.WriteLine($" {bucket} "); + } +} + +app.MapDefaultEndpoints(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.MapControllers(); + +app.MapPost("/api/sns/notification", async (HttpRequest request, IStorageService storage, ILogger logger) => +{ + using var reader = new StreamReader(request.Body); + var body = await reader.ReadToEndAsync(); + + using var doc = JsonDocument.Parse(body); + var type = doc.RootElement.GetProperty("Type").GetString(); - x.UsingAmazonSqs((context, cfg) => + if (type == "SubscriptionConfirmation") { - cfg.Host(awsRegion, h => + var subscribeUrl = doc.RootElement.GetProperty("SubscribeURL").GetString(); + logger.LogInformation(" : {Url}", subscribeUrl); + + using var client = new HttpClient(); + await client.GetAsync(subscribeUrl); + + return Results.Ok(new { message = "Subscription confirmed" }); + } + + if (type == "Notification") + { + var messageJson = doc.RootElement.GetProperty("Message").GetString(); + var employee = JsonSerializer.Deserialize(messageJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (employee != null) { - h.Config(new AmazonSQSConfig { ServiceURL = awsServiceUrl }); - h.Config(new AmazonSimpleNotificationServiceConfig { ServiceURL = awsServiceUrl }); - h.AccessKey(awsAccessKey); - h.SecretKey(awsSecretKey); - }); - - cfg.ConfigureEndpoints(context); - }); + var fileName = $"employee_{employee.Id}.json"; + var content = JsonSerializer.SerializeToUtf8Bytes(employee); + await storage.SaveFileAsync(bucketName, fileName, content); + logger.LogInformation(" {Id} MinIO", employee.Id); + } + } + + return Results.Ok(); }); -var app = builder.Build(); +app.MapGet("/api/files", async (IStorageService storage) => +{ + var files = await storage.ListFilesAsync(bucketName); + return Results.Ok(new { count = files.Count(), files }); +}); -app.MapDefaultEndpoints(); -app.MapGet("/health", () => "Healthy"); +app.MapGet("/api/files/{fileName}", async (string fileName, IStorageService storage) => +{ + var content = await storage.GetFileAsync(bucketName, fileName); + if (content == null) return Results.NotFound(); + var json = Encoding.UTF8.GetString(content); + return Results.Text(json, "application/json"); +}); app.MapGet("/api/files/exists/{fileName}", async (string fileName, IStorageService storage) => { @@ -59,4 +102,10 @@ return exists ? Results.Ok() : Results.NotFound(); }); +app.MapGet("/api/files/{fileName}/metadata", async (string fileName, IStorageService storage) => +{ + var metadata = await storage.GetFileMetadataAsync(bucketName, fileName); + return metadata != null ? Results.Ok(metadata) : Results.NotFound(); +}); + app.Run(); \ No newline at end of file diff --git a/CompanyEmployee.FileService/Properties/launchSettings.json b/CompanyEmployee.FileService/Properties/launchSettings.json index 75948cf6..7927ee78 100644 --- a/CompanyEmployee.FileService/Properties/launchSettings.json +++ b/CompanyEmployee.FileService/Properties/launchSettings.json @@ -21,7 +21,7 @@ "https": { "commandName": "Project", "dotnetRunMessages": true, - "launchBrowser": true, + "launchBrowser": false, "applicationUrl": "https://localhost:7277;http://localhost:5194", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/CompanyEmployee.FileService/Services/IStorageService.cs b/CompanyEmployee.FileService/Services/IStorageService.cs index 0fba6522..af21ce81 100644 --- a/CompanyEmployee.FileService/Services/IStorageService.cs +++ b/CompanyEmployee.FileService/Services/IStorageService.cs @@ -11,7 +11,27 @@ public interface IStorageService public Task SaveFileAsync(string bucketName, string key, byte[] content); /// - /// Проверяет существование файла в хранилище. + /// Проверяет существование файла. /// public Task FileExistsAsync(string bucketName, string key); -} \ No newline at end of file + + /// + /// Возвращает список всех файлов в бакете. + /// + public Task> ListFilesAsync(string bucketName); + + /// + /// Возвращает содержимое файла. + /// + public Task GetFileAsync(string bucketName, string key); + + /// + /// Возвращает метаданные файла. + /// + public Task GetFileMetadataAsync(string bucketName, string key); +} + +/// +/// Метаданные файла. +/// +public record FileMetadata(string Name, long Size, DateTime LastModified); \ No newline at end of file diff --git a/CompanyEmployee.FileService/Services/MinioStorageService.cs b/CompanyEmployee.FileService/Services/MinioStorageService.cs index 84c7c9f1..af9f7e61 100644 --- a/CompanyEmployee.FileService/Services/MinioStorageService.cs +++ b/CompanyEmployee.FileService/Services/MinioStorageService.cs @@ -6,41 +6,36 @@ namespace CompanyEmployee.FileService.Services; /// /// Реализация хранилища через MinIO. /// -public class MinioStorageService : IStorageService +/// Клиент MinIO. +/// Логгер. +public class MinioStorageService( + IMinioClient minioClient, + ILogger logger) : IStorageService { - private readonly IMinioClient _minioClient; - private readonly ILogger _logger; - - public MinioStorageService(IMinioClient minioClient, ILogger logger) - { - _minioClient = minioClient; - _logger = logger; - } - public async Task SaveFileAsync(string bucketName, string key, byte[] content) { try { - var bucketExists = await _minioClient.BucketExistsAsync(new BucketExistsArgs().WithBucket(bucketName)); + var bucketExists = await minioClient.BucketExistsAsync(new BucketExistsArgs().WithBucket(bucketName)); if (!bucketExists) { - await _minioClient.MakeBucketAsync(new MakeBucketArgs().WithBucket(bucketName)); - _logger.LogInformation("Создан бакет {BucketName}", bucketName); + await minioClient.MakeBucketAsync(new MakeBucketArgs().WithBucket(bucketName)); + logger.LogInformation("Создан бакет {BucketName}", bucketName); } using var stream = new MemoryStream(content); - await _minioClient.PutObjectAsync(new PutObjectArgs() + await minioClient.PutObjectAsync(new PutObjectArgs() .WithBucket(bucketName) .WithObject(key) .WithStreamData(stream) .WithObjectSize(stream.Length) .WithContentType("application/json")); - _logger.LogInformation("Файл {Key} загружен в MinIO", key); + logger.LogInformation("Файл {Key} загружен в MinIO", key); } catch (Exception ex) { - _logger.LogError(ex, "Ошибка загрузки файла {Key}", key); + logger.LogError(ex, "Ошибка загрузки файла {Key}", key); throw; } } @@ -49,14 +44,77 @@ public async Task FileExistsAsync(string bucketName, string key) { try { - await _minioClient.StatObjectAsync(new StatObjectArgs() + await minioClient.StatObjectAsync(new StatObjectArgs() .WithBucket(bucketName) .WithObject(key)); return true; } - catch + catch (Exception ex) { + logger.LogWarning(ex, "Ошибка при проверке файла {Key}", key); return false; } } + + public async Task> ListFilesAsync(string bucketName) + { + var files = new List(); + try + { + var args = new ListObjectsArgs() + .WithBucket(bucketName) + .WithRecursive(true); + + await foreach (var item in minioClient.ListObjectsEnumAsync(args)) + { + files.Add(item.Key); + } + + logger.LogInformation("Получен список файлов из бакета {BucketName}, найдено {Count} файлов", bucketName, files.Count); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка при получении списка файлов из бакета {BucketName}", bucketName); + } + return files; + } + + public async Task GetFileAsync(string bucketName, string key) + { + try + { + using var memoryStream = new MemoryStream(); + var args = new GetObjectArgs() + .WithBucket(bucketName) + .WithObject(key) + .WithCallbackStream(stream => stream.CopyTo(memoryStream)); + + await minioClient.GetObjectAsync(args); + logger.LogInformation("Файл {Key} загружен из MinIO", key); + return memoryStream.ToArray(); + } + catch (Exception ex) + { + logger.LogError(ex, "Ошибка при получении файла {Key}", key); + return null; + } + } + + public async Task GetFileMetadataAsync(string bucketName, string key) + { + try + { + var args = new StatObjectArgs() + .WithBucket(bucketName) + .WithObject(key); + + var stat = await minioClient.StatObjectAsync(args); + return new FileMetadata(key, stat.Size, stat.LastModified); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Ошибка при получении метаданных файла {Key}", key); + return null; + } + } } \ No newline at end of file diff --git a/CompanyEmployee.FileService/appsettings.json b/CompanyEmployee.FileService/appsettings.json index 46ff1a6a..ac6c3c11 100644 --- a/CompanyEmployee.FileService/appsettings.json +++ b/CompanyEmployee.FileService/appsettings.json @@ -5,14 +5,7 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*", - "AWS": { - "ServiceURL": "http://localhost:4566", - "Region": "us-east-1", - "AccessKeyId": "test", - "SecretAccessKey": "test" - }, - "S3": { - "BucketName": "employee-data" + "ConnectionStrings": { + "minio": "Endpoint=http://localhost:9000;AccessKey=minioadmin;SecretKey=minioadmin" } } \ No newline at end of file