اشتراکها
اشتراکها
معرفی Microservices
امروز یکی از برنامهها (برنامه ASP.Net) با مشکل زیر مواجه شده بود:
پیغام خطا:
اتصال با سرور اس کیوال قطع شده است. لطفا با مسئول مربوطه هماهنگ نمائید.
SQLErr:4060
این خطا به معنای عدم امکان باز کردن دیتابیس است.
در طی این مدت با موارد زیادی از این دست (مشکلات مختلف عدم امکان برقراری ارتباط با اس کیوال سرور) برخورد داشتم که خلاصه تمام آنها تابع زیر شده است:
public void CheckSQLServerStat(Exception ex)
{
try
{
SqlException ar = (SqlException) ex;
switch (ar.Number)
{
case 2:
case 11:
case 17:
case 40:
case 4060:
case 1326:
case 17142:
case 18456:
HttpContext.Current.Response.Write("<br/>" + "اتصال با سرور اس کیوال قطع شده است. لطفا با مسئول مربوطه هماهنگ نمائید." + "<br/> SQLErr:" + ar.Number + "<br/>");
break;
}
}catch{}
}
به management studio اس کیوال سرور که مراجعه کردم، علامت خاصی کنار نام دیتابیس نبود فقط برخلاف سایر دیتابیسها که آیکون + مربوط به باز شدن tree آن وجود دارد، این یک مورد آنرا نداشت. بر روی نام دیتابیس کلیک راست کردم و انتخاب خواص، خطای زیر نمایش داده شد:
------------------------------
An exception occurred while executing a Transact-SQL statement or batch. (Microsoft.SqlServer.ConnectionInfo)
------------------------------
Database 'dbName' cannot be opened due to inaccessible files or insufficient memory or disk space. See the SQL Server errorlog for details. (Microsoft SQL Server, Error: 945)
------------------------------
use master;
alter database dbName set OFFLINE;
alter database dbName set online;
مطالب
AngularJS #4
در این قسمت قصد دارم تا یک سیستم ارسال دیدگاه را به کمک Angular پیاده سازی کنم. هدف از این مثال؛ آشنایی با چند Directive توکار Angular و همچنین آموختن چگونگی کار با سرویس http$ برای ارتباط با سرور است.
کدهای HTML زیر را در نظر بگیرید:
<div ng-app="myApp"> <div ng-controller="CommentCtrl"> <div ng-repeat="comment in comments"> <div style="float:right;cursor:pointer;" ng-click="remove(comment.Id,$index);">X</div> <a href="#"> <img style="width:32px;" ng-src="/Content/user.gif" alt="{{comment.Name}}"> </a> <div> <h4>{{comment.Name}}</h4> {{comment.CommentBody}} </div> </div> <div> <form action="/Comment/Add" method="post"> <div> <label for="Name">Name</label> <input id="Name" type="text" name="Name" ng-model="comment.Name" placeholder="Your Name" /> </div> <div> <label for="Email">Email</label> <input id="Email" type="text" name="Email" ng-model="comment.Email" placeholder="Your Email" /> </div> <div> <label for="CommentBody">Comment</label> <textarea id="CommentBody" name="CommentBody" ng-model="comment.CommentBody" placeholder="Your Comment"></textarea> </div> <button type="button" ng-click="addComment()">Send</button> </form> </div> </div> </div>
خب از ابتدا ساختار را مورد بررسی قرار میدهم و موارد ناآشنای آن را توضیح میدهم:
ng-app: خاصیت ng-app جز خواص پیش فرض HTML نیست و یک خاصیت سفارشی است که توسط Angular به صورت پیش فرض تعریف شده است. این خاصیت به Angular میگوید که کدام بخش از DOM باید توسط Angular مدیریت و پردازش شود. در اینجا div ای که با خاصیت ng-app مزین شده است به همراه تمامی عناصر فرزند آن توسط موتور پردازش گر DOM توکار مورد پردازش قرار گرفته و اصطلاحا کامپایل میشود. بله! اینجا از لفظ کامپایل شدن برای بیان این فرآیند استفاده کردم. هیچ کدام از این Directiveهای سفارشی به خودی خود برای مرورگر قابل تفسیر نیست و اینجاست که Angular وارد عمل شده و این Directiveها را به کدهای HTML و جاوا اسکریپت که برای مرورگر قابل فهم است تبدیل میکند. به همین جهت با ng-app مشخص میکنیم که کدام بخش از DOM باید توسط Angular تفسیر و مدیریت شود.شاید این سوال برای شما مطرح شده باشد که در مثال قبلی ng-app مقداری نداشت و برای تگ html تعریف شده بود. پاسخ این است که در مثال قبلی چون برنامهی ما دارای یک ماژول بیشتر نبود میتوانستیم از مقدار دهی ng-app صرف نظر کنیم؛ اما در این مثال ما قصد داریم کمی هم مفهوم ماژول را در Angular بررسی کنیم. در نتیجه در این مثال برنامهی ما از ماژولی به نام myApp تشکیل شده است. دلیل اینکه در این مثال ng-app بر روی یک div تعریف شده است این است که همین قسمت از DOM توسط Angular تفسیر شود برای ما کفایت میکند. هنگامی ng-app را بر روی html تعریف میکنیم که قصد داشته باشیم کل صفحه توسط Angular تفسیر شود.
ng-controller: در Angular کنترلرها تابع سازندهی کلاسهای سادهی جاوا اسکریپتی هستند که به کمک آنها بخشی از صفحه را مدیریت میکنیم. این که کدام بخش از صفحه توسط کدام کلاس کنترل و مدیریت شود، توسط ng-controller مشخص میشود. در اینجا هم عنصری که با ng-controller مشخص شده به همراه تمامی فرزندانش، توسط کلاس جاوا اسکریپتی به نام CommentCtrl مدیریت میشود. در حقیقت ما به کمک ng-controller مشخص میکنیم که کدام قسمت از View توسط کدام Controller مدیریت میشود. مرسوم است که در Angular نام کنترلرها با Ctrl خاتمه یابد.
ng-repeat: همهی نظرات دارای یک قالب html یکسان هستند که به ازای دادههای متفاوت تکرار شده اند. اگر میخواستیم نظرات را استفاده از موتور نمایشی Razor نشان دهیم از یک حلقهی foreach استفاده میکردیم. خبر خوب این است که ng-repeat هم دقیقا به مانند حلقهی foreach عمل میکند.در اینجا عبارت comment in comments دقیقا برابر با آن چیزی است که در یک حلقهی foreach مینوشتیم. Comments در اینجا یک لیست به مانند آرایه ای از comment هست که در کنترلر مقدار دهی شده است. پس اگر با حلقهی foreach مشکلی نداشته باشید با مفهوم ng-repeat هم مشکلی نخواهید داشت و دقیقا به همان شکل عمل مینماید.
ng-click: همان طور که گفتیم Directiveهای تعریف شده میتوانند یک event سفارشی نیز باشند. ng-click هم یک Directive تو کار است که توسط Angular به صورت پیش فرض تعریف شده است. کاملا مشخص است که یک تابع به نام remove تعریف شده است که به هنگام کلیک شدن، فراخوانی میشود. دو پارامتر هم به آن ارسال شده است. اولین پارامتر Id دیدگاه مورد نظر است تا به سرور ارسال شود و از پایگاه داده حذف شود. دومین پارامتر index$ است که یک متغیر ویژه است که توسط Angular در هر بار اجرای حلقهی ng-repeat مقدارش یک واحد افزایش مییابد. index$ هم به تابع remove ارسال میشود تا بتوان فهمید در سمت کلاینت کدام نظر باید حذف شود.
ng-src: از این Directive برای مشخص کردن src عکسها استفاده میشود. البته در این مثال چندان تفاوتی بین ng-src و src معمولی وجود ندارد. ولی اگر آدرس عکس به صورت Content/{{comment.Name}}.gif میبود دیگر وضع فرق میکرد. چرا که مرورگر با دیدن آدرس در src سعی به لود کردن آن عکس میکند و در این حالت در لود کردن آن عکس با شکست روبرو میشود. ng-src سبب میشود تا در ابتدا آدرس عکس توسط Angular تفسیر شود و سپس آن عکس توسط مرورگر لود شود.
{{comment.Name}}: آکلودهای دوتایی برای انقیاد داده (Data Binding) با view-model استفاده میشود. این نوع اقیاد داده در مثالهای قبلی مورد بررسی قرار گرفته است و نکتهی بیشتری در اینجا مطرح نیست.
ng-model: به کمک ng-model میتوان بین متن داخل textbox و خاصیت شی مورد نظر انقیاد داده بر قرار کرد و هر دو طرف از تغییرات یکدیگر آگاه شوند. به این عمل انقیاد داده دوطرفه (Two-Way Data-Binding) میگویند.برای مثال textbox مربوط به نام را به comment.Name و textbox مربوط به email را به comment.Email مقید(bind) شده است. هر تغییری که در محتوای هر کدام از طرفین صورت گیرد دیگری نیز از آن تغییر با خبر شده و آن را نمایش میدهد.
تا به اینجای کار قالب مربوط به HTML را بررسی کردیم. حال به سراغ کدهای جاوا اسکریپت میرویم:
var app = angular.module('myApp', []); app.controller('CommentCtrl', function ($scope, $http) { $scope.comment = {}; $http.get('/Comment/GetAll').success(function (data) { $scope.comments = data; }) $scope.addComment = function () { $http.post("/Comment/Add", $scope.comment).success(function () { $scope.comments.push({ Name: $scope.comment.Name, CommentBody: $scope.comment.CommentBody }); $scope.comment = {}; }); }; $scope.remove = function (id, index) { $http.post("/Comment/Remove", { id: id }).success(function () { $scope.comments.splice(index, 1); }); }; });
در تعریف ng-app اگر به یاد داشته باشید برای آن مقدار myApp در نظر گرفته شده بود. در اینجا هم ما به کمک متغیر سراسری angular که توسط خود کتابخانه تعریف شده است، ماژولی به نام myApp را تعریف کرده ایم. پارامتر دوم را فعلا توضیح نمیدهم، ولی در این حد بدانید که برای تعریف وابستگیهای این ماژول استفاده میشود که من آن را برابر یک آرایه خالی قرار داده ام.
در سطر بعد برای ماژول تعریف شده یک controller تعریف کرده ام. شاید دفعهی اول است که تعریف کنترلر به این شکل را مشاهده میکنید. اما چرا به این شکل کنترلر تعریف شده و به مانند قبل به شکل تابع سازندهی کلاس تعریف نشده است؟
پاسخ این است که اکثر برنامه نویسان از جمله خودم دل خوشی از متغیر سراسری ندارند. در شکل قبلی تعریف کنترلر، کنترلر به شکل یک متغیر سراسری تعریف میشد. اما استفاده از ماژول برای تعریف کنترلر سبب میشود تا کنترلرهای ما روی هوا تعریف نشده باشند و هر یک در جای مناسب خود باشند. به این شکل مدیریت کدهای برنامه نیز سادهتر بود. مثلا اگر کسی از شما بپرسد که فلان کنترلر کجا تعریف شده است؛ به راحتی میگویید که در فلان ماژول برنامه تعریف و مدیریت شده است.
در تابعی که به عنوان کنترلر تعریف شده است، دو پارامتر به عنوان وابستگی درخواست شده است. scope$ که برای ارتباط با view-model و انقیاد داده به کار میرود و http$ که برای ارتباط با سرور به کار میرود. نمونهی مناسب هر دوی این پارامترها توسط سیستم تزریق وابستگی تو کار angular در اختیار کنترلر قرار میگیرد.
قبلا چگونگی استفاده از scope$ برای اعمال انقیاد داده توضیح داده شده است. نکتهی جدیدی که مطرح است چگونگی استفاده از سرویس http$ برای ارتباط با سرور است. سرویس http $ دارای 4 متد put ، post ، get و delete است.
واقعا استفاده از این سرویس کاملا واضح و روشن است. در متد addComment وقتی که دیدگاه مورد نظر اضافه شد، به آرایهی کامنتها یک کامنت جدید میافزاییم و چون انقیاد داده دو طرفه است، بالافاصله دیدگاه جدید نیز در view به نمایش در میآید.کار تابع remove هم بسیار ساده است. با استفاده از index ارسالی، دیدگاه مورد نظر را از آرایهی کامنتها حذف میکنیم و ادامهی کار توسط انقیاد داده دو طرفه انجام میشود.
همان طور که مشاهده میشود مفاهیم انقیاد داده دو طرفه و تزریق وابستگی خودکار سرویسهای مورد نیاز، کار با angularjs را بسیار ساده و راحت کرده است. اصولا در بسیاری از موارد احتیاجی به باز اختراع چرخ نیست و کتابخانهی angular آن را برای ما از قبل تدارک دیده است.
کدهای این مثال ضمیمه شده است. این کدها در Visual Studio 2013 و به کمک ASP.NET MVC 5 و Entity Framework 6 نوشته شده است. سعی شده تا مثال نوشته شده به واقعیت نزدیک باشد. اگر دقت کنید مدل کامنت در مثالی که نوشتم به گونه ای است که دیدگاههای چند سطحی به همراه پاسخ هایش مد نظر بوده است. به عنوان تمرین نمایش درختی این گونه دیدگاهها را به کمک Angular انجام دهید. کافیست Treeview in Angular را جست و جو کنید؛ مطمئنا به نتایج زیادی میرسید. گرچه در مثال ضمیمه شده اگر جست و جو کنید من پیاده سازیش را انجام دادم. هدف از جست و جو در اینترنت مشاهده این است که بیشتر مسائل در Angular از پیش توسط دیگران حل شده است و احتیاجی نیست که شما با چالشهای جدیدی دست و پنجه نرم کنید.
پس به عنوان تمرین، دیدگاههای چند سطحی به همراه پاسخ که نمونه اش را در همین سایتی که درحال مشاهده آن هستید میبینید را به کمک AngularJS پیاده سازی کنید.
در مقالهی بعدی چگونگی انتقال منطق تجاری برنامه از کنترلر به لایه سرویس و چگونگی تعریف سرویس جدید را مورد بررسی قرار میدهم.
در قسمتهای Blazor Server مثال این سری، با روش کار با سرویسهای سمت سرور برنامه، آشنا شدیم. در این نوع برنامهها، فقط کافی است اصل سرویس مدنظر را مستقیما در کامپوننتهای Razor تزریق کرد و سپس میتوان به نحو متداولی با آنها کار کرد؛ اما در برنامههای Blazor WASM خیر! به این نوع برنامههای سمت کلاینت باید همانند برنامههای React ، Angular ، Vue و یا حتی برنامههای مبتنی بر jQuery نگاه کرد. در تمام فناوریهای سمت کلاینت، این درخواستهای Ajax ای هستند که با سرویسهای یک Web API سمت سرور، ارتباط برقرار کرده، اطلاعاتی را به آنها ارسال و یا دریافت میکنند. در برنامههای Blazor WASM نیز باید به همین ترتیب عمل کرد و در اینجا HttpClient دات نت، جایگزین برای مثال jQuery Ajax ، Fetch API و یا XMLHttpRequest استاندارد میشود (البته jQuery Ajax در اصل یک محصور کنندهی استاندارد XMLHttpRequest است که برای اولین بار توسط مایکروسافت در برنامهی Outlook web access معرفی شد).
ایجاد سرویس سمت کلاینت دریافت اطلاعات اتاقها از Web API
در قسمت 24، HotelRoomController را تکمیل کردیم که کار آن، بازگشت اطلاعات تمام اتاقها و یا یک اتاق مشخص به کلاینت است. اکنون میخواهیم در ادامهی قسمت قبل، اگر کاربری بر روی دکمهی Go صفحهی اول رزرو اتاقی کلیک کرد، لیست تمام اتاقهای تعریف شده را به او نمایش دهیم. به همین جهت نیاز به سرویس سمت کلاینتی داریم که بتواند با این Web API endpoint کار کند:
این سرویس را در پوشهی Services پروژهی BlazorWasm.Client ایجاد کردهایم که HotelRoomDTO خود را از پروژهی BlazorServer.Models دریافت میکند. به این ترتیب میتوان مدلی را بین یک Web API سمت سرور و یک سرویس سمت کلاینت، به اشتراک گذاشت. بنابراین پروژهی کلاینت، باید ارجاعی را به پروژهی BlazorServer.Models.csproj نیز داشته باشد.
در ادامه اینترفیس فوق را به صورت زیر پیاده سازی میکنیم:
توضیحات:
- HttpClient یکی از سرویسهای تنظیم شدهی در فایل Program.cs پروژههای سمت کلاینت است. بنابراین میتوان آنرا از طریق تزریق به سازندهی این سرویس، به دست آورد.
- در اینجا برای دریافت اطلاعات JSON دریافتی از سمت سرور و سپس Deserialize خودکار آن به لیستی از DTO تعریف شده، از متد جدید GetFromJsonAsync استفاده شدهاست. این مورد جزو تازههای NET 5x. است.
- در اینجا استفاده از کلاس UriBuilderExt را نیز جهت تشکیل یک URL دارای کوئری استرینگ، مشاهده میکنید. هیچگاه نباید URL نهایی را از طریق جمع زدن اجزای آن به سمت سرور ارسال کرد؛ از این جهت که اجزای آن باید URL-encoded شوند؛ وگرنه در سمت سرور قابلیت پردازش نخواهند داشت. در ادامه تعریف کلاس جدید UriBuilderExt را مشاهده میکنید:
- در اینجا توسط متد AddParameter، کار افزودن کوئری استرینگها به یک Url از پیش مشخص، انجام میشود. کار encoding نهایی به صورت خودکار توسط HttpUtility استاندارد دات نت، انجام خواهد شد.
- تاریخهای ارسالی به سمت سرور را با فرمت yyyy'-'MM'-'dd تبدیل رشته کردیم. این قالب، یکی از قالبهای پذیرفته شدهاست.
- جهت سهولت استفادهی از سرویس فوق و همچنین مدلهای برنامه، فضای نام آنها را به فایل BlazorWasm.Client\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامهی سمت کلاینت، قابل دسترسی شوند:
- در آخر این سرویس جدید را باید به لیست سرویسهای برنامه معرفی کرد تا قابلیت تزریق در کامپوننتها را پیدا کند:
چند اصلاح جزئی در کنترلرها و سرویسهای سمت سرور
در Url نهایی فوق، دو پارامتر جدید checkInDate و checkOutDate هم وجود دارند. به همین جهت این دو را به اکشن متدهای کنترلر HotelRoom:
و همچنین سرویس سمت سرور IHotelRoomService نیز اضافه میکنیم:
البته فعلا پیاده سازی خاصی ندارند و آنها را در قسمتهای بعد مورد استفاده قرار خواهیم داد.
تنظیمات ویژهی HttpClient برنامهی سمت کلاینت
سرویس ClientHotelRoomService فوق، از HttpClient تزریق شدهی در سازندهی خود استفاده میکند که BaseAddress خود را مطابق تنظیمات ابتدایی برنامه، از HostEnvironment دریافت میکند. در اینجا علاقمندیم تا بجای این تنظیم پیشفرض، فایل جدید appsettings.json را به پوشهی BlazorWasm.Client\wwwroot\appsettings.json کلاینت اضافه کرده (محل قرارگیری آن در برنامههای سمت کلاینت، داخل پوشهی wwwroot است و نه در داخل پوشهی ریشهی اصلی پروژه):
و از این تنظیم جدید به عنوان BaseAddress برنامهی Web API استفاده کنیم که روش آنرا در کدهای ذیل مشاهده میکنید:
تکمیل کامپوننت دریافت لیست تمام اتاقها
در قسمت قبل، کامپوننت خالی HotelRooms.razor را تعریف کردیم. کاربران پس از کلیک بر روی دکمهی Go صفحهی اول، به این کامپوننت هدایت میشوند. اکنون میخواهیم، لیست تمام اتاقها را در این کامپوننت، از Web API برنامه دریافت کنیم:
در اینجا در ابتدا سعی میشود تا HomeModel، از Local Storage که در قسمت قبل آنرا تنظیم کردیم، خوانده شود. سپس با استفاده از متد GetHotelRoomsAsync، لیست اتاقها را از Web API دریافت میکنیم. تمام این عملیات آغازین نیز باید در روال رویدادگران OnInitializedAsync صورت گیرند.
روش اجرای پروژههای Blazor WASM
تا اینجا اگر برنامهی سمت کلاینت را توسط دستور dotnet watch run اجرا کنیم، هرچند صفحهی خالی نمایش لیست اتاقها ظاهر میشود، اما یک خطای fetch error را هم دریافت خواهیم کرد؛ چون نیاز است ابتدا پروژهی Web API را اجرا کرد و سپس پروژهی WASM را.
برای ساده سازی اجرای همزمان این دو پروژه، اگر از ویژوال استودیوی کامل استفاده میکنید، بر روی نام Solution کلیک راست کرده و از منوی ظاهر شده، گزینهی «Set Startup projects» را انتخاب کنید. در صفحه دیالوگ ظاهر شده، گزینهی «multiple startup projects» را انتخاب کرده و از لیست پروژههای موجود، دو پروژهی Web API و WASM را انتخاب کنید و Action مقابل آنها را به Start تنظیم کنید. در اینجا حتی میتوان ترتیب اجرای این پروژهها را هم تغییر داد. در این حالت زمانیکه بر روی دکمهی Run، در ویژوال استودیو کلیک میکنید، هر دو پروژه را با هم برای شما اجرا خواهد کرد.
نکتهی مهم! در این حالت هم برنامهی کلاینت نمیتواند با برنامهی Web API ارتباط برقرار کند! چون شماره پورت iisExpress درج شدهی در فایل appsettings.json آن، باید به شماره sslPort مندرج در فایل Properties\launchSettings.json پروژهی Web API تغییر داده شود که برای نمونه در اینجا این عدد 44314 است:
و یا اگر میخواهید پروژه را از طریق NET Core CLI. با اجرای دستور dotnet watch run اجرا کنید ... به صورت پیشفرض نمیشود! چون برای اینکار باید به پوشهی ریشهی پروژههای Web API و WASM وارد شد و دوبار دستور یاد شده را به صورت مجزا اجرا کرد. در این حالت، هر دو پروژه، بر روی پورت پیشفرض 5001 اجرا میشوند. روش تغییر این پورت، مراجعه به فایل Properties\launchSettings.json این پروژهها است. برای مثال همان پورت پیشفرض 5001 را که در فایل appsettings.json انتخاب کردیم، ثابت نگه میداریم. یعنی فایل launchSettings.json پروژهی Web API را ویرایش نمیکنیم. اما این پورت را در پروژهی کلاینت برای مثال به عدد 5002 تغییر میدهیم تا برنامهی کلاینت، بر روی پورت پیشفرض برنامهی Web API اجرا نشود:
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-27.zip
ایجاد سرویس سمت کلاینت دریافت اطلاعات اتاقها از Web API
در قسمت 24، HotelRoomController را تکمیل کردیم که کار آن، بازگشت اطلاعات تمام اتاقها و یا یک اتاق مشخص به کلاینت است. اکنون میخواهیم در ادامهی قسمت قبل، اگر کاربری بر روی دکمهی Go صفحهی اول رزرو اتاقی کلیک کرد، لیست تمام اتاقهای تعریف شده را به او نمایش دهیم. به همین جهت نیاز به سرویس سمت کلاینتی داریم که بتواند با این Web API endpoint کار کند:
namespace BlazorWasm.Client.Services { public interface IClientHotelRoomService { public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate); public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate); } }
در ادامه اینترفیس فوق را به صورت زیر پیاده سازی میکنیم:
namespace BlazorWasm.Client.Services { public class ClientHotelRoomService : IClientHotelRoomService { private readonly HttpClient _httpClient; public ClientHotelRoomService(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate) { throw new NotImplementedException(); } public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate) { // How to url-encode query-string parameters properly var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, "/api/hotelroom")) .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}") .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}") .Uri; return _httpClient.GetFromJsonAsync<IEnumerable<HotelRoomDTO>>(uri); } } }
- HttpClient یکی از سرویسهای تنظیم شدهی در فایل Program.cs پروژههای سمت کلاینت است. بنابراین میتوان آنرا از طریق تزریق به سازندهی این سرویس، به دست آورد.
- در اینجا برای دریافت اطلاعات JSON دریافتی از سمت سرور و سپس Deserialize خودکار آن به لیستی از DTO تعریف شده، از متد جدید GetFromJsonAsync استفاده شدهاست. این مورد جزو تازههای NET 5x. است.
- در اینجا استفاده از کلاس UriBuilderExt را نیز جهت تشکیل یک URL دارای کوئری استرینگ، مشاهده میکنید. هیچگاه نباید URL نهایی را از طریق جمع زدن اجزای آن به سمت سرور ارسال کرد؛ از این جهت که اجزای آن باید URL-encoded شوند؛ وگرنه در سمت سرور قابلیت پردازش نخواهند داشت. در ادامه تعریف کلاس جدید UriBuilderExt را مشاهده میکنید:
using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; using BlazorServer.Models; using BlazorWasm.Client.Utils; using System; using System.Collections.Specialized; using System.Web; namespace BlazorWasm.Client.Utils { public class UriBuilderExt { private readonly NameValueCollection _collection; private readonly UriBuilder _builder; public UriBuilderExt(Uri uri) { _builder = new UriBuilder(uri); _collection = HttpUtility.ParseQueryString(string.Empty); } public UriBuilderExt AddParameter(string key, string value) { _collection.Add(key, value); return this; } public Uri Uri { get { _builder.Query = _collection.ToString(); return _builder.Uri; } } } }
- تاریخهای ارسالی به سمت سرور را با فرمت yyyy'-'MM'-'dd تبدیل رشته کردیم. این قالب، یکی از قالبهای پذیرفته شدهاست.
- جهت سهولت استفادهی از سرویس فوق و همچنین مدلهای برنامه، فضای نام آنها را به فایل BlazorWasm.Client\_Imports.razor اضافه میکنیم تا در تمام کامپوننتهای برنامهی سمت کلاینت، قابل دسترسی شوند:
@using BlazorWasm.Client.Services @using BlazorServer.Models
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IClientHotelRoomService, ClientHotelRoomService>(); // ... } } }
چند اصلاح جزئی در کنترلرها و سرویسهای سمت سرور
در Url نهایی فوق، دو پارامتر جدید checkInDate و checkOutDate هم وجود دارند. به همین جهت این دو را به اکشن متدهای کنترلر HotelRoom:
namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]")] [ApiController] public class HotelRoomController : ControllerBase { // ... [HttpGet] public async Task<IActionResult> GetHotelRooms(DateTime? checkInDate, DateTime? checkOutDate) { // ... } [HttpGet("{roomId}")] public async Task<IActionResult> GetHotelRoom(int? roomId, DateTime? checkInDate, DateTime? checkOutDate) { // ... } } }
namespace BlazorServer.Services { public interface IHotelRoomService : IDisposable { Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDate, DateTime? checkOutDate); Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate); // ... } }
تنظیمات ویژهی HttpClient برنامهی سمت کلاینت
سرویس ClientHotelRoomService فوق، از HttpClient تزریق شدهی در سازندهی خود استفاده میکند که BaseAddress خود را مطابق تنظیمات ابتدایی برنامه، از HostEnvironment دریافت میکند. در اینجا علاقمندیم تا بجای این تنظیم پیشفرض، فایل جدید appsettings.json را به پوشهی BlazorWasm.Client\wwwroot\appsettings.json کلاینت اضافه کرده (محل قرارگیری آن در برنامههای سمت کلاینت، داخل پوشهی wwwroot است و نه در داخل پوشهی ریشهی اصلی پروژه):
{ "BaseAPIUrl": "https://localhost:5001/" }
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... // builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseAPIUrl")) }); // ... } } }
تکمیل کامپوننت دریافت لیست تمام اتاقها
در قسمت قبل، کامپوننت خالی HotelRooms.razor را تعریف کردیم. کاربران پس از کلیک بر روی دکمهی Go صفحهی اول، به این کامپوننت هدایت میشوند. اکنون میخواهیم، لیست تمام اتاقها را در این کامپوننت، از Web API برنامه دریافت کنیم:
@page "/hotel/rooms" @inject ILocalStorageService LocalStorage @inject IJSRuntime JsRuntime @inject IClientHotelRoomService HotelRoomService <h3>HotelRooms</h3> @code { HomeVM HomeModel = new HomeVM(); IEnumerable<HotelRoomDTO> Rooms = new List<HotelRoomDTO>(); protected override async Task OnInitializedAsync() { try { var model = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking); if (model is not null) { HomeModel = model; } else { HomeModel.NoOfNights = 1; } await LoadRooms(); } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } } private async Task LoadRooms() { Rooms = await HotelRoomService.GetHotelRoomsAsync(HomeModel.StartDate, HomeModel.EndDate); } }
روش اجرای پروژههای Blazor WASM
تا اینجا اگر برنامهی سمت کلاینت را توسط دستور dotnet watch run اجرا کنیم، هرچند صفحهی خالی نمایش لیست اتاقها ظاهر میشود، اما یک خطای fetch error را هم دریافت خواهیم کرد؛ چون نیاز است ابتدا پروژهی Web API را اجرا کرد و سپس پروژهی WASM را.
برای ساده سازی اجرای همزمان این دو پروژه، اگر از ویژوال استودیوی کامل استفاده میکنید، بر روی نام Solution کلیک راست کرده و از منوی ظاهر شده، گزینهی «Set Startup projects» را انتخاب کنید. در صفحه دیالوگ ظاهر شده، گزینهی «multiple startup projects» را انتخاب کرده و از لیست پروژههای موجود، دو پروژهی Web API و WASM را انتخاب کنید و Action مقابل آنها را به Start تنظیم کنید. در اینجا حتی میتوان ترتیب اجرای این پروژهها را هم تغییر داد. در این حالت زمانیکه بر روی دکمهی Run، در ویژوال استودیو کلیک میکنید، هر دو پروژه را با هم برای شما اجرا خواهد کرد.
نکتهی مهم! در این حالت هم برنامهی کلاینت نمیتواند با برنامهی Web API ارتباط برقرار کند! چون شماره پورت iisExpress درج شدهی در فایل appsettings.json آن، باید به شماره sslPort مندرج در فایل Properties\launchSettings.json پروژهی Web API تغییر داده شود که برای نمونه در اینجا این عدد 44314 است:
{ "iisSettings": { "iisExpress": { "applicationUrl": "http://localhost:62930", "sslPort": 44314 } } }
{ "BlazorWasm.Client": { "applicationUrl": "https://localhost:5002;http://localhost:5003", } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-27.zip
مطالب
سیستمهای توزیع شده در NET. - بخش دوم - چرا یک سخت افزار به تنهایی پاسخگوی نیازمندیهای ما نیست؟
قبل از شروع به بحث در مورد سیستمهای توزیع شده، بهتر است ابتدا به سوالی اساسی که اساس بوجود آمدن سیستمهای توزیع شده است، پاسخ دهیم:
چرا یک سخت افزار به تنهایی پاسخگوی همه نیازهای ما نیست؟
همه میدانیم که در یک هستهی از پردازنده، چیزی بعنوان پردازش موازی وجود ندارد. هر هسته در هر لحظه میتواند یک پردازش را انجام دهد و این سرعت بالای در پردازش عملیات جاری و سوئیچ کردن بین دیگر عملیات است که حس موازی اجرا شدن آنها را به ما میدهد. یعنی در صورتیکه بخواهیم در یک سخت افزار با پردازندهی تک هستهای، برنامه نویسی موازی انجام بدهیم، در واقع هیچیک از عملیات ما بصورت موازی انجام نمیشوند. زمان پردازشی پردازنده، بر اساس تعداد عملیات و اولویت آنها، بین آنها تقسیم میشود. هر لحظه یکی از آنها اجرا میشود و با اتمام زمان اجرایش، نوبت به بعدی میرسد تا جاییکه تمام آنها به اتمام برسند. در این حالت پردازنده تک هستهای، برای 2 کار زمان صرف میکند؛ اول اجرای عملیات جاری و دوم سوئیچ کردن به عملیات بعدی.
راه حل چیست؟
ساده است. با افزایش تعداد هستههای پردازنده، سیستم ما قادر است تعداد عملیات بیشتری را بصورت همزمان انجام دهد که این عملیات به تعداد هستههای پردازنده، واقعا بصورت همزمان انجام میشوند. یعنی هر هسته در هر لحظه یک پردازش را میتواند بصورت جداگانه از سایر هستهها انجام دهد.
اینجا بود که نیازمندیهای ما باعث شدند سخت افزارها پیچیدهتر شوند. البته پیچیدگی بود که باعث تکامل آنها شد. تا اینجا برای انجام تعداد عملیات بیشتر میتوانیم سخت افزار را ارتقاء دهیم. همچنین در اینجا بود که مفهوم Parallel Systems تکامل پیدا کرد؛ سیستمهایی که توانایی اجرای همزمان چند عملیات را داشتند که همه آنها از یک حافظه، بصورت مشترک استفاده میکردند.
مشکل سیستمهای Parallel مشخص است. کارآیی این نوع سیستم، کاملا به سخت افزار و نوع پیاده سازی آنها وابسته است. یعنی در صورت نیاز به کارآیی بیشتر، تنها راه ارتقاء سخت افزار و بهینه کردن کدهاست. اما این روال را تا کجا میتوانیم انجام دهیم؟
برای روشن شدن مشکل بالا بیایید یک Web Application را بر روی یک سخت افزار اجرا کنیم. در یک Web Application یک Thread Pool شامل مجموعهای از Threadها میباشد که هر Thread وظیفه اجرای یک درخواست را بر عهده دارد. یعنی با دریافت یک درخواست، یک Thread از این مجموعه کم میشود و وظیفه پاسخ دهی به آن در خواست را بر عهده میگیرد. تعداد Thread هایی که در یک Thread Pool میباشند نیز وابسته به تعداد هستههای پردازنده میباشد. برای این تعداد بصورت پیشفرض مقداری در نظر گرفته میشود که بیشترین کارآیی را در یک هسته داشته باشد؛ مثلا در ASP.NET بصورت پیشفرض به ازای هر هستهی از CPU، تعداد 20 Thread به این مجموعه اضافه میشود. یعنی ما در یک پردازنده 2 هستهای تنها میتوانیم تعداد 40 درخواست را بصورت همزمان دریافت کنیم. در صورتیکه تعداد در خواستها در یک لحظه بیشتر از این تعداد باشد، تمام درخواستهای اضافی در صف دریافت قرار میگیرند تا یکی از این Threadها به درخواست خودش پاسخ دهد و به Thread Pool بازگردد و آماده اجرای درخواست بعدی باشد.
حال با فرض اینکه بصورت میانگین به هر درخواست در مدت 2 ثانیه پاسخ داده شود و در طول هر 2 ثانیه ما تعداد 200 درخواست جدید دریافت کنیم، یعنی در هر 2 ثانیه تعداد 160 درخواست در صف پردازش درخواست باقی میمانند. یعنی در مدت 10 ثانیه تعداد 800 درخواست پردازش نشده در این صف وجود دارند. در صورتیکه این روال ادامه پیدا کند، صف پردازش بزرگتر و بزرگتر میشود؛ تا جایی که دیگر حافظهای برای دریافت درخواستهای جدید نباشد. اینجاست که سیستم ما از دسترس خارج میشود. پس تصمیم میگیریم سخت افزار خود را ارتقاء دهیم و کدهای خود را نیز بهینه کنیم. مثلا جاییکه عملیات I/O را انجام میدهیم، برای استفادهی بهینه از Threadهای موجود، کدهای خود را بصورت Async اجرا کنیم.
تا حدودی مشکل ما فعلا حل شدهاست. بعد از مدتی بدلیل اضافه شدن نیازمندیهای جدید، تعدادکاربران فعال سیستم زیاد میشود و دوباره مشکل پوشش دادن تعداد بیشتر درخواست بوجود میآید. مجبوریم دوباره عملیات Scale-up یا Vertical scaling را انجام دهیم. بله؛ عملیاتی که ما در این سیستمها برای مقیاسپذیری انجام میدهیم، اصطلاحا Vertical scaling یا Scale-up نام دارد. یعنی با افزایش تعداد کاربران یا تعداد درخواست، کدها بهینهتر و سخت افزار ارتقاء پیدا میکند.
البته مثالی که ذکر شد به هیچ وجه با دنیای واقعی قابل مقایسه نیست. ممکن است شما سرویسی بسیار حیاتی را پیاده سازی کرده باشید که در شرایط خاص، هزاران یا میلیونها کاربر بصورت همزمان بخواهند درخواستهای خود را برای شما ارسال کنند. در این حالت شما دو راه دارید؛ اول اینکه مرتبا سرویس بسیار حیاتی خود را از دسترس خارج کنید و منتظر بمانید تا حجم تعداد درخواستهای کاربران کاهش یابد و یا مجبورید سخت افزار سرور خود را آنقدر ارتقاء دهید، تا این تعداد درخواست را بصورت همزمان پوشش دهد. واقعا هزینه تهیه کردن این سرور چقدر است؟
فرض کنید از سمت پایگاه داده نیز با مشکل روبرو شدهایم. حجم دادههای ما روبه افزایش است. فضای حافظهی آزاد تنها Server ی که دادههای ما را ذخیره میکند، رو به اتمام است. چاره چیست؟ آن را ارتقا دهیم؟ بله برای مدتی سرور را از دسترس خارج کرده و فضای آزاد را افزایش میدهیم. در این بین تمام سرویسهای ما که وابسته به این سرور هستند، از دسترس خارج میشوند. این کار برای دادههایی که ذاتا همیشه رو به افزایش هستند، چقدر باید تکرار شوند؟ چقدر باید هزینه کنیم تا این دادهها که تمام سرویسهای ما به آنها وابسته هستند، با مشکل مواجه نشوند، یا کارآیی بازیابی آنها پایین نیاید؟
حال بیاید از زاویه دیگری به ماجرا نگاه کنیم ما یک سرویس بسیار حساس با تعداد کاربران زیادی را داریم. از دسترس خارج شدن این سرویس برای ما بسیار هزینه بر است. اما تنها سرور بسیار قوی ما که برای آن هزینهی بسیار زیادی را پرداخت کردهایم، با مشکلی مواجه شده که ممکن است زمان زیادی برای رفع مشکل آن صرف شود. بله باز سرویس از دسترس خارج شده و ما با مشکل بسیار جدی مواجه شدهایم که ممکن است آیندهی سرویس بسیار مهم را به خطر بیاندازد. چاره چیست؟ مثلا تعدادی سرور مشابه سرور اصلی را خریداری کنیم و در صورتیکه سرور اصلی با مشکل جدی مواجه شد از آنها استفاده کنیم. بله هزینه چند برابر شد. فرض کنید به هر دلیل، برق قسمتی که شما این سرورها را نگهداری میکنید، قطع شد! چه راهکاری را دارید؟ واقعیتی که باید بپذیریم این است که چون ما یک سرور را برای اجرای Application خودمان داریم، در هرصورت اگر این سرور با مشکلی مواجه شود، تمام سرویسهای ما با خطری جدی مواجه میشوند و ما نیز در صورتیکه بخواهیم این چرخهی معیوب را ادامه دهیم، تنها هر بار صورت مسئله را پاک میکنیم. بهتر است روش جدیدی را برای این مشکل بیابیم.
اینجاست که مفهوم سیستمهای توزیع شده به کمک سیستمهای Parallel میآید و مفهوم Scale-up یا Vertical scaling با مفهموم Horizontal Scaling یا Scale-out ادغام میشود. در قسمت بعدی، با مفاهیم، خصوصیات و اصطلاحات موجود در این سیستمها آشنا میشویم.
چرا یک سخت افزار به تنهایی پاسخگوی همه نیازهای ما نیست؟
همه میدانیم که در یک هستهی از پردازنده، چیزی بعنوان پردازش موازی وجود ندارد. هر هسته در هر لحظه میتواند یک پردازش را انجام دهد و این سرعت بالای در پردازش عملیات جاری و سوئیچ کردن بین دیگر عملیات است که حس موازی اجرا شدن آنها را به ما میدهد. یعنی در صورتیکه بخواهیم در یک سخت افزار با پردازندهی تک هستهای، برنامه نویسی موازی انجام بدهیم، در واقع هیچیک از عملیات ما بصورت موازی انجام نمیشوند. زمان پردازشی پردازنده، بر اساس تعداد عملیات و اولویت آنها، بین آنها تقسیم میشود. هر لحظه یکی از آنها اجرا میشود و با اتمام زمان اجرایش، نوبت به بعدی میرسد تا جاییکه تمام آنها به اتمام برسند. در این حالت پردازنده تک هستهای، برای 2 کار زمان صرف میکند؛ اول اجرای عملیات جاری و دوم سوئیچ کردن به عملیات بعدی.
حال در صورتیکه تعداد عملیاتی که باید در سیستم بصورت همزمان انجام شوند بیشتر شود، زمانیکه پردازنده باید برای سوئیچ صرف کند نیز بیشتر شده و در نتیجه زمان اجرای عملیات بیشتر میشود و آنها دیرتر به اتمام میرسند. با بیشتر شدن تعداد این عملیات، کار به جایی میرسد که دیگر پردازنده هیچ زمانی را برای پردازش یک عملیات ندارد و باید تمام وقت خود را با سوئیچ کردن بین آنها صرف کند. بله؛ ما سیستمی را طراحی کردهایم که شامل مجموعهای از عملیات است که هیچ یک اجرا نمیشوند!
راه حل چیست؟
ساده است. با افزایش تعداد هستههای پردازنده، سیستم ما قادر است تعداد عملیات بیشتری را بصورت همزمان انجام دهد که این عملیات به تعداد هستههای پردازنده، واقعا بصورت همزمان انجام میشوند. یعنی هر هسته در هر لحظه یک پردازش را میتواند بصورت جداگانه از سایر هستهها انجام دهد.
اینجا بود که نیازمندیهای ما باعث شدند سخت افزارها پیچیدهتر شوند. البته پیچیدگی بود که باعث تکامل آنها شد. تا اینجا برای انجام تعداد عملیات بیشتر میتوانیم سخت افزار را ارتقاء دهیم. همچنین در اینجا بود که مفهوم Parallel Systems تکامل پیدا کرد؛ سیستمهایی که توانایی اجرای همزمان چند عملیات را داشتند که همه آنها از یک حافظه، بصورت مشترک استفاده میکردند.
مشکل سیستمهای Parallel مشخص است. کارآیی این نوع سیستم، کاملا به سخت افزار و نوع پیاده سازی آنها وابسته است. یعنی در صورت نیاز به کارآیی بیشتر، تنها راه ارتقاء سخت افزار و بهینه کردن کدهاست. اما این روال را تا کجا میتوانیم انجام دهیم؟
برای روشن شدن مشکل بالا بیایید یک Web Application را بر روی یک سخت افزار اجرا کنیم. در یک Web Application یک Thread Pool شامل مجموعهای از Threadها میباشد که هر Thread وظیفه اجرای یک درخواست را بر عهده دارد. یعنی با دریافت یک درخواست، یک Thread از این مجموعه کم میشود و وظیفه پاسخ دهی به آن در خواست را بر عهده میگیرد. تعداد Thread هایی که در یک Thread Pool میباشند نیز وابسته به تعداد هستههای پردازنده میباشد. برای این تعداد بصورت پیشفرض مقداری در نظر گرفته میشود که بیشترین کارآیی را در یک هسته داشته باشد؛ مثلا در ASP.NET بصورت پیشفرض به ازای هر هستهی از CPU، تعداد 20 Thread به این مجموعه اضافه میشود. یعنی ما در یک پردازنده 2 هستهای تنها میتوانیم تعداد 40 درخواست را بصورت همزمان دریافت کنیم. در صورتیکه تعداد در خواستها در یک لحظه بیشتر از این تعداد باشد، تمام درخواستهای اضافی در صف دریافت قرار میگیرند تا یکی از این Threadها به درخواست خودش پاسخ دهد و به Thread Pool بازگردد و آماده اجرای درخواست بعدی باشد.
حال با فرض اینکه بصورت میانگین به هر درخواست در مدت 2 ثانیه پاسخ داده شود و در طول هر 2 ثانیه ما تعداد 200 درخواست جدید دریافت کنیم، یعنی در هر 2 ثانیه تعداد 160 درخواست در صف پردازش درخواست باقی میمانند. یعنی در مدت 10 ثانیه تعداد 800 درخواست پردازش نشده در این صف وجود دارند. در صورتیکه این روال ادامه پیدا کند، صف پردازش بزرگتر و بزرگتر میشود؛ تا جایی که دیگر حافظهای برای دریافت درخواستهای جدید نباشد. اینجاست که سیستم ما از دسترس خارج میشود. پس تصمیم میگیریم سخت افزار خود را ارتقاء دهیم و کدهای خود را نیز بهینه کنیم. مثلا جاییکه عملیات I/O را انجام میدهیم، برای استفادهی بهینه از Threadهای موجود، کدهای خود را بصورت Async اجرا کنیم.
تا حدودی مشکل ما فعلا حل شدهاست. بعد از مدتی بدلیل اضافه شدن نیازمندیهای جدید، تعدادکاربران فعال سیستم زیاد میشود و دوباره مشکل پوشش دادن تعداد بیشتر درخواست بوجود میآید. مجبوریم دوباره عملیات Scale-up یا Vertical scaling را انجام دهیم. بله؛ عملیاتی که ما در این سیستمها برای مقیاسپذیری انجام میدهیم، اصطلاحا Vertical scaling یا Scale-up نام دارد. یعنی با افزایش تعداد کاربران یا تعداد درخواست، کدها بهینهتر و سخت افزار ارتقاء پیدا میکند.
البته مثالی که ذکر شد به هیچ وجه با دنیای واقعی قابل مقایسه نیست. ممکن است شما سرویسی بسیار حیاتی را پیاده سازی کرده باشید که در شرایط خاص، هزاران یا میلیونها کاربر بصورت همزمان بخواهند درخواستهای خود را برای شما ارسال کنند. در این حالت شما دو راه دارید؛ اول اینکه مرتبا سرویس بسیار حیاتی خود را از دسترس خارج کنید و منتظر بمانید تا حجم تعداد درخواستهای کاربران کاهش یابد و یا مجبورید سخت افزار سرور خود را آنقدر ارتقاء دهید، تا این تعداد درخواست را بصورت همزمان پوشش دهد. واقعا هزینه تهیه کردن این سرور چقدر است؟
فرض کنید از سمت پایگاه داده نیز با مشکل روبرو شدهایم. حجم دادههای ما روبه افزایش است. فضای حافظهی آزاد تنها Server ی که دادههای ما را ذخیره میکند، رو به اتمام است. چاره چیست؟ آن را ارتقا دهیم؟ بله برای مدتی سرور را از دسترس خارج کرده و فضای آزاد را افزایش میدهیم. در این بین تمام سرویسهای ما که وابسته به این سرور هستند، از دسترس خارج میشوند. این کار برای دادههایی که ذاتا همیشه رو به افزایش هستند، چقدر باید تکرار شوند؟ چقدر باید هزینه کنیم تا این دادهها که تمام سرویسهای ما به آنها وابسته هستند، با مشکل مواجه نشوند، یا کارآیی بازیابی آنها پایین نیاید؟
حال بیاید از زاویه دیگری به ماجرا نگاه کنیم ما یک سرویس بسیار حساس با تعداد کاربران زیادی را داریم. از دسترس خارج شدن این سرویس برای ما بسیار هزینه بر است. اما تنها سرور بسیار قوی ما که برای آن هزینهی بسیار زیادی را پرداخت کردهایم، با مشکلی مواجه شده که ممکن است زمان زیادی برای رفع مشکل آن صرف شود. بله باز سرویس از دسترس خارج شده و ما با مشکل بسیار جدی مواجه شدهایم که ممکن است آیندهی سرویس بسیار مهم را به خطر بیاندازد. چاره چیست؟ مثلا تعدادی سرور مشابه سرور اصلی را خریداری کنیم و در صورتیکه سرور اصلی با مشکل جدی مواجه شد از آنها استفاده کنیم. بله هزینه چند برابر شد. فرض کنید به هر دلیل، برق قسمتی که شما این سرورها را نگهداری میکنید، قطع شد! چه راهکاری را دارید؟ واقعیتی که باید بپذیریم این است که چون ما یک سرور را برای اجرای Application خودمان داریم، در هرصورت اگر این سرور با مشکلی مواجه شود، تمام سرویسهای ما با خطری جدی مواجه میشوند و ما نیز در صورتیکه بخواهیم این چرخهی معیوب را ادامه دهیم، تنها هر بار صورت مسئله را پاک میکنیم. بهتر است روش جدیدی را برای این مشکل بیابیم.
اینجاست که مفهوم سیستمهای توزیع شده به کمک سیستمهای Parallel میآید و مفهوم Scale-up یا Vertical scaling با مفهموم Horizontal Scaling یا Scale-out ادغام میشود. در قسمت بعدی، با مفاهیم، خصوصیات و اصطلاحات موجود در این سیستمها آشنا میشویم.
در ابتدا اجازه بدهید تعریف درستی از این دو واژه، ارائه کنیم.
DTO (Data Transfer Object)
به بیان خیلی ساده، DTOها برای انتقال اطلاعات استفاده میشوند؛ پس هیچ منطق و رفتاری در این اشیاء تعریف نمیشود .اگر در DTO منطقی پیاده سازی شود، دیگر به آن DTO گفته نمیشود. اجازه بدید منظورمان را از منطق یا رفتار مشخص کنیم. منطق یا رفتار، همان متدهایی هستند که در نوع داده خود تعریف میکنیم. در #C، یک DTO تنها از خصوصیتها (Properties) که از بلوکهای Get و Set تشکیل شدهاند، ساخته میشود. البته بدون کدهایی جهت اعتبار سنجی (Validation) مقادیر.
سؤال: وضعیت attribute ها و Metadataها چه میشود؟
خیلی غیر معمول نیست که از metadataها در DTO، بهمنظور اعتبار سنجی یا اهداف خاص، استفاده کنیم. بعضی از attributeها هیچ رفتاری را به DTOها اضافه نمیکنند؛ ولی استفاده از DTOها را در بخشهای دیگر سیستم، سادهتر میکنند. در نتیجه هیچکدام از attribute ها و metadataها، شرایط DTO بودن را نقض نمیکنند.
مدلهای دیگری مثل ViewModelsها و API Modelها چه میشوند؟
واژه DTO خیلی مبهم است. تنها چیزی که بیان میکند این است که شیء است و فقط و فقط شامل اطلاعات است و رفتاری ندارد. در این تعریف دربارهی کاربرد مورد نظر یک DTO چیزی گفته نشده. در بسیاری از معماریها، DTO نقش خاصی را ایفا میکند. بطور مثال در معماری MVC، از DTOها برای انقیاد داده (Binding) و ارسال اطلاعات به یک View استفاده میکنند. به همین خاطر این DTOها بعنوان ViewModel، در معماری MVC شناخته میشوند که رفتاری را در خود تعریف نمیکنند و تنها فرمت اطلاعات مورد انتظار یک View را مهیا میکنند.
پس در این سناریوی خاص، ViewModel نوعی DTO میباشد. اما باید دقت داشته باشید، همه ViewModelها را نمیتوان DTO محسوب کرد؛ مثلا در معماری MVVM، ویوو مدلهای تعریف شده، شامل رفتار هم میباشند. حتی در معماری MVC نیز گاهی اوقات منطقی به ViewModelها اضافه میشود که دیگر به آنها DTO نمیگوییم.
در صورت امکان، نام DTOها را بر اساس استفادهی آنها تعیین کنید. بطور مثال کلاسی با نام FoodDTO، مشخص نمیکند که این نوع، کجا و چگونه قرار است در معماری برنامه شما مورد استفاده قرار بگیرید؛ برعکس نامگذاری به صورت FoodViewModels کاربرد آن را صراحتا بیان میکند.
مثالی از DTO در زبان سی شارپ :
public class ProductViewModel { public int ProductId { get; set; } public string Name { get; set; } public string Description { get; set; } public string ImageUrl { get; set; } public decimal UnitPrice { get; set; } }
کپسوله سازی و DTO ها
کپسوله سازی، یکی از اصول برنامه نویسی شیءگرا میباشد. اما این کپسوله سازی به DTOها اعمال نمیشوند. به این علت که هدف کپسوله سازی، پنهان کردن فرآیند پشت صحنهی ذخیره سازی اطلاعات است؛ اما در DTO هیچ فرآیندی پیاده سازی نشده و نباید هیچ State پنهانی وجود داشته باشد. پس بحث Encapsulation در DTO منتفی است. پس کار را برای خودتان سخت نکنید؛ با تعریف private setter ها یا تبدیل کردن DTO به یک شیء غیرقابل تغییر (immutable). شما باید بهراحتی بتوانید عملیات ایجاد، نوشتن و خواندن DTOها را انجام دهید؛ همچنین باید بتوانید عملیات سریالایز کردن بر روی DTOها را بدون فرآیند سفارشی اضافهای، انجام دهید.
Field ها یا Property ها
سؤالی که مطرح میشود این است که وقتی کپسوله سازی در DTO مفهومی ندارد، چرا باید همیشه از property ها استفاده کنیم؟ چرا از فیلدها استفاده نکنیم (فیلدهای public )؟
ما میتوانیم هم از property استفاده کنیم و هم از fieldها؛ اما بعضی از فریم ورکها که کار Serialization را انجام میدهند، فقط با property ها کار میکنند. بنابراین بسته به نیاز خودتان، از fieldهای عمومی یا propertyها استفاده کنید. اما عموما از Property استفاده میکنند. البته در این پیوند، پرسش و پاسخ مفصلی در این رابطه وجود دارد.
غیرقابل تغییر بودن (Immutability) و نوع رکورد ( Record Type )
غیرقابل تغییر بودن، یکی از مزیتهای مهم در توسعه نرم افزار است. اما همانطور که در مثل بالا بیان شد، نیازی به غیرقابل تغییر کردن DTOها نیست. با ارائه رکورد در سی شارپ 9 شرایط کمی تغییر کرد. شاید عبارت مخفف دیگری که اضافه شده Data transfer Records یا (DTRs) است. یکی از روشهای تعریف DTR در سی شارپ 9، به شکل زیر است:
public record ProductDTO(int Id, string Name, string Description);
البته روش دیگری هم وجود دارد که شما propertyها را تعریف کنید و از طریق سازنده، مقدار دهی شوند. ویژگی جدید init-only این امکان را فراهم میکند که فقط در زمان مقدار دهی اولیه (initialization)، خصوصیات مقداردهی شوند و در ادامهی چرخه حیات شیء، property ها فقط خواندنی هستند. این ویژگی، recordها را غیر قابل تغییر میکند.
مثال:
public record ProductDTO { public int Id { get; init; } public string Name { get; init; } } var dto = new ProductDTO { Id = 1, Name = "some name" };
کلاسهای POCO یا همان Plain Old CLR/C# Object
شی Plain Old چیست؟ هر شیءای که Plain Old باشد، میتواند در هر جایی از برنامهی ما مورد استفاده قرار بگیرد؛ حتی در کلاسهای Test برنامه. این اشیاء هیچگونه وابستگی برای اجرا وظایف خود، به بانکهای اطلاعاتی و کتابخانههای ثالت ندارند.
برای درک بهتر این نوع کلاسها، به مثال زیر دقت کنید:
public class Product : DataObject<Product> { public Product(int id) { Id = id; InitializeFromDatabase(); } private void InitializeFromDatabase() { DataHelpers.LoadFromDatabase(this); } public int Id { get; private set; } // other properties and methods }
همانطور که مشاهده میکنید، این کلاس به متد استاتیکی برای کار با دیتابیس وابسته است؛ در نتیجه باعث میشود که کل کلاس، به وجود بانک اطلاعاتی وابسته شود. همچنین با ارث بری از کلاس پایهی دیگری، وابستگی به یک کتابخانهی ثالث ایجاد شدهاست. اجرای آزمون واحد برای چنین کلاسی، سبب fail شدن عملیات میشود. به این علت که ارتباط با بانک اطلاعاتی مورد نیاز متد DataHelpers، تامین نشدهاست. این شرایط، مثالی از الگوی Active Record Pattern میباشند. همچنین این کلاس دسترسی به منبع داده را در درون خود گنجانده است که این به معنای نقض اصل Persistence Ignorant (اصل Persistence Ignorance به طور خلاصه بیان میکند که در تحلیل و طراحی Business Logic به موضوع ذخیرهسازی (Persistence) فکر نکنید (تا جای ممکن) یا به عبارت دیگر، ذهن خود را درگیر پیچیدگیهای ذخیره سازی نکنید. برگرفته شده از breakpoint.blog.ir : روح الله دلپاک)می باشد. یکی از ویژگیهای POCO عدم نقض الگوی فوق است.
مثالی از POCO :
public class Product { public Product(int id) { Id = id; } private Product() { // required for EF } public int Id { get; private set; } // other properties and methods }
این کلاس یک POCO است:
- برای اجرای وظایف خود به فریم ورک ثالثی وابسته نیست.
- به کلاس پایهای ( Base class) نیاز ندارد.
- وابستگی به متد استاتیکی ندارد.
- می تواند در هر جایی از پروژه، نمونه سازی شود.
- اصل Persistence Ignorant را بیشتر رعایت کرده، نه بطور کامل؛ چون یک سازنده دارد که به کتابخانهی ثالثی نیازمند است (سازندهی بدون پارامتر که مورد نیاز EF میباشد).
POCO و DTO :
شاید این دو مفهموم گیج کننده باشند، ولی DTO همان POCO هست. اگر یک کلاس، DTO باشد، حتما POCO نیز هست. (مرور ویژگیهای دو مورد در بخشهای قبلی) ولی برعکس این وضعیت ممکن است صادق نباشد؛ مثال قبلی که در آن وابستگی به کتابخانهی ثالثی در سازندهی بدون پارامتر وجود داشت، DTO بودن را نقض میکرد. پس اگر هر دو حالت صادق بود، میتوان گفت این دو مفهوم یکی است.
میخواستم بدونم اگر در یک custom authorize attribute بخواهم roleهای کاربر فعلی را بدانم باید چکار کنم؟
در سیستم membership خیلی راحت مینوشتم:
اما در این معماری ای که شما نوشته اید چکار باید کرد؟ آیا باید
در سیستم membership خیلی راحت مینوشتم:
var currentUserRoles =System.Web.Security.Roles.GetRolesForUser().Select(u => u.ToLower()).ToList();
IApplicationRoleManager
را به attribute پاس داد که فکر نمیکنم ممکن باشد. ممکن است قدری راهنمایی بفرمایید؟بعد از اینکه برنامه الکترون آماده شد، لازم است آن را به فایلهای اجرایی پلتفرمهای مختلف تبدیل کنیم. برای اینکار بسته معروف Electron-packager را مورد استفاده قرار میدهیم. برای نصب آن به شکل زیر اقدام کنید:
بعد از اینکه نصب شد، در فایل package.json در قسمت scripts، خصوصیت جدیدتری را وارد میکنیم:
این دستور شامل حداقلهای آرگومانها میباشد. در تشریح این دستور باید گفت اولین آرگومان، نام دایرکتوری است که برنامه شما در آن نوشته شده است و حاوی فایل package.json است و با قرار دادن علامت "." دایرکتوری جاری را معرفی کردهایم. بعد از آن نام برنامه و فایل اجرایی برنامه است. بعد از آن فلگها آغاز میشوند که اولین فلگ مشخص میکند خروجی خود را برای چه پلتفرمی نیاز دارید و شامل مقادیر زیر میشود؛ یا اینکه عبارت all را برای در نظر گرفتن همه مقادیر وارد کنید.
بعد از اجرای این دستور برای اولین بار، ممکن است برای هر پلتفرم، کتابخانههای مربوطه را دانلود کند تا بر اساس آن، عمل بسته بندی را انجام دهد. سپس در دایرکتوری پروژه، دایرکتوریهای جدیدی را با نامهای مشخصی خواهید یافت.
یکی از دیگر فلگها که کاربردی میباشد، برای نادیده گرفتن ورورد یک سری پکیجها به بسته نهایی است که به طور پیش فرض جلوی ورود بستههای eletron-prebuilt و electron-packager را میگیرد. ولی اگر دوست دارید تا بستههای دیگری را نیز به این لیست اضافه کنید، دستو زیر را به کار ببرید:
فلگهای پر استفاده دیگر این بسته:
این دستور با در نظر گرفتن همه پلتفرمها و معماری آنها، برای دایرکتوری جاری اجرا شده و نام برنامه و دیگر اطلاعات را از طریق فایل package.json به دست میآورد.
npm install electron-packager --save-dev
"build":"electron-packager . myapp --platform=all --arch=all --overwrite"
darwin | سیستم عامل مکینتاش |
linux | سیستم عامل لینوکس |
win32 | سیستم عامل ویندوز |
فلگ بعدی معماری سیستم عامل را نشان میدهد که برای سیستمهای 32 بیتی مقدار ia32 و برای سیستمهای 64 بیتی مقدار x64 میباشد. ولی در صورتیکه همه مقادیر را در نظر دارید، میتوانید همانند خط بالا از مقدار all استفاده کنید.
در همه حالات بالا اگر فقط تعدادی از آنها را بخواهید وارد کنید، میتوانید هر عبارت را با , از هم جدا سازید؛ مانند darwin,linux که برای این دو پلتفرم تنها نسخه اجرایی تولید میشود.
فلگ آخر اجباری نیست، ولی برای دفعات بعدی بسیار مناسب است. اگر از قبل یک بسته بندی وجود دارد، بسته بندی جدید بر روی قبلیها رونویسی خواهد شد.
حال با دستور زیر در nodejs، عملیات بسته بندی را آغاز میکنیم:
npm run build
یکی از دیگر فلگها که کاربردی میباشد، برای نادیده گرفتن ورورد یک سری پکیجها به بسته نهایی است که به طور پیش فرض جلوی ورود بستههای eletron-prebuilt و electron-packager را میگیرد. ولی اگر دوست دارید تا بستههای دیگری را نیز به این لیست اضافه کنید، دستو زیر را به کار ببرید:
--ignore=node_modules/<package_name> یا --ignore=node_modules/electron_[0-9]*
فلگهای پر استفاده دیگر این بسته:
aap-version | نسخه برنامه |
app-copyright | متنی برای قانون کپی رایت |
asar | موقعی که برنامهای را بسته بندی میکنید، در دایرکتوری Resources/App، هنوز سورس برنامه وجود دارد که فایل اجرایی شما بدون آن قادر به ادامه فعالیت نیست. ولی اگر بخواهیم این سورس را در اختیار شخصی قرار ندهیم، باید از ویژگی asar استفاده کنیم. با استفاده از این فلگ، فایلی با نام app.asar جای این دایرکتوری ایجاد خواهد شد و دیگر نیازی نیست تا سورس برنامه همراه آن باشد. |
icon | در صورتیکه قصد استفاده از آیکنی بجز آیکون الکترون را دارید. |
out | به طور پیش فرض برنامه نهایی در دایرکتوری کاری پروژه اضافه میشود. در صورتیکه قصد دارید آنرا در دایرکتوری بجز دایرکتوری کاری قرار دهید، از این ویژگی استفاده کنید. |
version-string | این خصوصیت برای نسخه بندی برنامه است که فقط برای ویندوز کاربرد دارد و شامل خصوصیاتی چون نام محصول، نام سازنده، توصیف برنامه و ... میباشد:--version-string.ProductName="Product" Properties supported: - CompanyName - FileDescription - OriginalFilename - ProductName - InternalName |
prune | استفاده از این فلگ باعث میشود کلیه بستههای معرفی شده در dev-dependency به بسته نهایی اضافه نشوند |
دستور بسته بندی بالا را نیز میتوان به طور خلاصهتر نیز نوشت :
electron-packager . --all