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/11] =?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 661f118..419e4ce 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 0000000..0e3c34e --- /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 0000000..0ee49eb --- /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 0000000..e69de29 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/11] =?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/11] =?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/11] =?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/11] =?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 cb48241..3a39d83 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 0000000..683c5a1 --- /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 0000000..4a6b1c1 --- /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 0000000..aa93f31 --- /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 0000000..2db4f09 --- /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 0000000..ef7b6c3 --- /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 0000000..b0b4e31 --- /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 0000000..0c208ae --- /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 0000000..10f68b8 --- /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 0000000..9b9aac1 --- /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 0000000..909c1aa --- /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 0000000..6c680ac --- /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 0000000..0c208ae --- /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 0000000..31c092a --- /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 0000000..2150e37 --- /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 0000000..8663815 --- /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 0000000..d808731 --- /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 0000000..112c128 --- /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/11] 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 aa93f31..0000000 --- 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/11] =?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 0000000..3db94ff --- /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 0000000..d651307 --- /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 0000000..9bc3092 --- /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/11] =?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 683c5a1..9caa856 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 3db94ff..b01ef5f 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 0000000..88efd00 --- /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 9b9aac1..a679d5c 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 909c1aa..10f9023 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 2150e37..fa71b7a 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/11] =?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 d1fe7ab..bc85f89 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 9caa856..a64e4e9 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 ef7b6c3..14794eb 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/11] 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 dcaa5eb..0000000 --- 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/11] =?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 a64e4e9..9e70639 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 4a6b1c1..0000000 --- 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 b01ef5f..365d910 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 88efd00..9ed0984 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 14794eb..100cc7e 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 d651307..11b2956 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 0000000..523bf77 --- /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 b0b4e31..812b234 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 9bc3092..000463b 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 0000000..e9c68aa --- /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 a679d5c..6e02e8d 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 10f9023..1a392b4 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 8663815..8bdc4ae 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 d808731..c8234c2 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 112c128..e23f6c6 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.