مطالب
معماری میکروسرویس‌ها
برنامه‌های بزرگ سمت سرور که با تعداد بسیار زیادی کاربر و داده سر و کار دارند، نباید فقط درگیر پاسخگویی سریع و فراهم کردن وب سرویس‌ها برای پلت‌فرم‌های مختلف باشند. این برنامه‌ها باید بتوانند به سادگی رشد کرده، ارتقاء پیدا کنند و به روز شوند. برای ساخت و توسعه این نوع برنامه‌ها، دو مدل معماری وجود دارد: یکی  معماری Monolithic و دیگری معماری Microservices. برای شناخت معماری Microservices، ابتدا بایستی با معماری Monolithic آشنا شد.


 معماری Monolithic چیست؟ 

در معماری Monolithic بخش‌های مختلف برنامه سمت سرور از جمله پردازش پرداخت آنلاین، مدیریت حساب‌ها، اعلان‌ها و سایر بخش‌ها همگی در یک واحد منفرد جمع شده‌اند. به عبارتی اگر برنامه تحت وب که در سرور قرار دارد به صورت یک جا با تمام متعلقات خود برای پاسخ به درخواست‌های سمت کلاینت، کار با پایگاه داده و انجام سایر الگوریتم‌ها اجرا شود، این برنامه از معماری Monolithic استفاده میکند.


مشکلات معماری Monolithic

  •  در معماری Monolithic زمانیکه ترافیک برنامه در سمت سرور افزایش پیدا میکند، باید برای پاسخگویی، اندازه را افزایش داد. یعنی باید برنامه تحت وب خود را بر روی سرورهای مختلف مجددا اجرا نمود. بخشی به نام Load Balancer، وظیفه توزیع درخواست‌ها را به سرورهای مختلف که بر روی هر یک، یک نسخه از برنامه در حال اجرا است، به عهده دارد. بر اساس توضیحی که از این معماری ارایه شد، در هر یک از این اجرا‌ها، کل برنامه با تمام متعلقاتی که دارد، فارغ از اینکه به همه آنها نیاز است یا نه، از منابع سرور استفاده میکند.

  • در معماری Monolithic برنامه‌ها بر اساس یک زبان برنامه‌نویسی مشخص، برای یک فریم ورک مشخص نوشته می‌شوند. این برنامه‌ها اصطلاحا چند سکویی نیستند و کامپوننت‌های نوشته شده برای آنها فقط در فریم ورک جاری قابل استفاده مجدد هستند.
  • ممکن است برای هر تغییر ریز و درشت در برنامه‌های این معماری، نیاز به Build و Deploy مجدد کل برنامه باشد که احتمال از دسترس خارج شدن برنامه هم وجود دارد.
  • اگر بخشی از برنامه از کار بیافتد، ممکن است باعث از کار افتادن کل برنامه یا بخشهایی از آن شود. 


معماری Microservices

معماری Microservices راه نجات از مشکلات معماری Monolithic است. در معماری Microservices، برنامه سمت سرور به سرویس‌های مختلفی تقسیم میشود. هر سرویس یک فرآیند پردازشی مستقل است که به عنوان یکی از قابلیت‌های خاص برنامه سمت سرور به حساب می‌آید. به عنوان مثال یک سرویس وظیفه پرداخت‌ها را به عهده دارد و دیگری بطور مستقل برای مدیریت حساب‌ها استفاده می‌شود. برنامه‌های نوشته شده با این معماری اجباری برای اجرا شدن در سرورهای جداگانه را ندارند، مگر اینکه یک سرویس، شرایط خاصی از جمله مصرف بالای RAM یا نیاز به پردازش ویژه و زیاد در CPU را داشته باشد. در اینصورت بهتر است که سرویس از یک سرور مجزا اجرا شود. لازم است که سرویس‌ها در بستر شبکه با یکدیگر در ارتباط باشند. 

 در دیاگرام بالا میشود اینطور تصور کرد که Service1، یک وب سرور است که با مرورگر برای دریافت درخواست‌ها در ارتباط است و باقی سرویس‌ها حکم API  برای عملیات‌های مختلف را دارند. 


 ارزش معماری Microservices

  • از آنجایی که سرویس‌ها از طریق زبان مشترک شبکه با یکدیگر در ارتباط هستند، میشود آنها را با زبانهای برنامه‌نویسی مختلف و بر روی فریم‌فرک‌های متفاوت نوشت. 
  • بدیهی است که با این معماری، هر سرویس را میشود به صورت جداگانه ایجاد کرد و تغییر داد که باعث سرعت در به روزرسانی و فرآیند گسترش برنامه میشود.
  • مانیتور کردن سرویس‌ها ساده‌تر خواهد بود. از آنجایی که هر سرویس به صورت یک پردازش جداگانه اجرا خواهد شد، تعیین اینکه هر سرویس از چه منابعی و به چه اندازه‌ای استفاده میکند، آسان‌تر خواهد بود.
  • از آنجایی که این سرویس‌ها از طریق شبکه در تبادل هستند، میشود از آنها در سایر برنامه‌ها مجدداً استفاده کرد. 


افزایش یک سرویس خاص

 یکی از با ارزش‌ترین قابلیت‌های معماری Microservices، افزایش یک سرویس، که به عنوان مثال فقط یک وهله از آن در حال اجراست، به دو یا سه وهله جداگانه است؛ بدون آنکه نیاز باشد سرویس‌های در ارتباط با آنها نیز وهله سازیهای اضافه‌ای داشته باشند. این حالت در دیاگرام زیر قابل مشاهده است. 

 در دیاگرام بالا از سرویس یک، دو وهله، در دو سرور جداگانه ایجاد شده است که Load Balancer ترافیک ورودی را بین آنها تقسیم میکند. باقی سرویس‌ها به همان تعداد که بودند باقی می‌مانند.


مشکلات معماری Microservices

  • از آنجایی که برنامه‌های سمت سرور نوشته شده با این معماری به سرویس‌های مختلفی تقسیم میشوند، گسترش و تنظیمات آنها می‌تواند کاری وقت گیر و طاقت فرسایی باشد.
  • از آنجایی که ارتباط بین سرویس‌ها در بستر شبکه انجام می‌شود، انتظار کندی عملکرد سرویس‌ها دور از ذهن نیست.
  • به دلیل ارتباطات شبکه‌ای، احتمال آسیب پذیری‌های امنیتی در این نوع برنامه‌ها بیشتر است.
  • نوشتن سرویس‌هایی که در بستر شبکه با سایر سرویس‌ها در ارتباط هستند سختی و مشکلات خود را دارد. برنامه‌نویس در این شرایط، درگیر برقراری ارتباط، رمزگذاری داده‌ها در صورت نیاز و تبدیل آنها می‌شود.
  • به دلیل مجزا بودن بخش‌های مختلف برنامه، مانیتور کردن و ردیابی عملکرد سرویس‌ها، یکی از کارهای اصلی توسعه دهنده یا استفاده کننده از برنامه است. 
  • در مجموع سرعت برنامه‌های نوشته شده با معماری Microservices کندتر از برنامه‌های نوشته شده با معماری Monolithic است. دلیل آن محیط اجرایی برنامه‌ها است. برنامه‌هایی با معماری Monolithic بر روی حافظه سرور پردازش می‌شوند.


چه زمانی از معماری Microservices استفاده کنیم؟

در واقع قاعده مشخصی برای انتخاب بین این دو معماری وجود ندارد. شاید بهترین دلیل برای استفاده از این معماری زمانی است که تیم توسعه دهنده به این نتیجه برسد که خصوصیات معماری Monolithic برای آنها مشکل به حساب می‌آید.
اگر تیم توسعه دهنده تصمیم بگیرد که از معماری Monolithic به نوع Microservices تغییر مسیر دهد، نیازی به نوشتن کل برنامه از ابتدا نیست. در این شرایط می‌توان فقط کامپوننت‌هایی را که دردسر ساز شده‌اند، به نوع سرویسی آن تبدیل کرد. به این نوع برنامه‌های سمت سروری که بخش اصلی برنامه به صورت Monolithic ولی برخی از عملکردهای خاص آن به صورت سرویسی نوشته شده باشد، اصطلاحا معماری Microservices با هسته Monolithic گفته می‌شود.

 

 مدیریت داده‌ها: 

در معماری Microservices، هر سرویس می‌تواند پایگاه داده خود را برای ذخیره داده‌ها داشته باشد و یا اینکه از یک پایگاه داده مرکزی بهره ببرد. استفاده از پایگاه داده‌ی مجزای برای هر سرویس، مشکلات خود را دارد. باید بین سرویس‌های مختلف، همگام سازی صورت بگیرد که در این صورت، اگر یکی از سرویس‌ها از کار بیافتد، سرویس‌های وابسته به آن که داده‌ها بین آنها در تبادل هستند، دچار مشکل می‌شود و مسئله می‌تواند به سایر سرویس‌ها که به صورت زنجیر وار به داده‌های در حال تبادل یکدیگر وابسته هستند، سرایت کند. همچنین در این روش داده‌های تکراری وجود خواهند داشت و در نهایت کدنویسی برای این سیستم مشکل خواهد بود. 
در نتیجه بهتر این است که سرویس‌ها با یک پایگاه داده مرکزی سر و کار داشته باشند و اگر سرویس خاصی نیاز داشته باشد که داده‌های بخصوص خود را تولید کند و نمی‌خواهد آن را با سایر سرویس‌ها به اشتراک بگذارد، می‌تواند پایگاه داده مخصوص به خود را داشته باشد. نکته مهم دیگر این است که سرویس‌ها نباید به صورت مستقیم با پایگاه داده مرکزی در ارتباط باشند؛ بلکه به سرویسی مجزا، به نام "سرویس پایگاه داده" که وظیفه فراهم کردن API‌های مخصوص کار با پایگاه داده مرکزی را به عهده دارد، نیاز است.


پیاده‌سازی معماری Microservices‌ها توسط فریم‌فرک Seneca

Seneca یک فریم ورک Node.js است که برای ساخت برنامه‌های سمت سروری با معماری Microservices و هسته Monolithic استفاده می‌شود. در مطلب بعدی به این فریم‌ورک نگاهی گذرا خواهیم داشت.
نظرات مطالب
EF Code First #12
- من در مورد الگوی مخزن در قسمت 11 این سری بحث کردم (کامنت‌های آن‌را هم بخوانید)؛ همچنین این مباحث در مورد EF Code first است و نه db first و نه EF 4. به علاوه این لینک‌هایی که مطرح کردید مثلا نمونه code project، داخل به اصطلاح BLL خودش، پر است از وهله سازی Context و من در این مطلب توضیح دادم که چرا اینکار غلط است و چگونه استفاده از یک تراکنش برای چندین عملیات مرتبط را زیر سؤال می‌برد.
- اون اینترفیس IUnitOfWork مطرح شده در مثال جاری، وابستگی خاصی به DataLayer نداره. می‌تونه در لایه سرویس هم تعریف بشه (منظور این است می‌تونه در یک اسمبلی و پروژه جداگانه هم قرار بگیره و مشکلی نیست). اما DataLayer باید بتونه در حین تزریق وابستگی‌ها وهله‌ای از IUnitOfWork رو فراهم کنه تا به اون معنا ببخشه؛ به همین جهت Context برنامه باید آن‌را پیاده سازی کند تا توسط StructureMap قابل شناسایی و استفاده شود.
اما نهایتا وهله سازی اینترفیس یاد شده توسط DAL صورت می‌گیره. uow به خودی خود موجودیتی نداره. در اینجا مثلا EF هست که به اون معنا می‌بخشه و سبب وهله سازی آن خواهد شد. هرچند به ظاهر برنامه با اینترفیس‌ها کار می‌کند اما تزریق وابستگی‌ها است که به این اینترفیس‌ها موجودیت می‌بخشد و سبب دسترسی به وهله‌ای که قرار داد ارائه شده توسط آن‌ها را پیاده سازی کرده می‌شود.
- در یک سیستم nTier هم مباحث ذکر شده در این قسمت، جاری است. مثلا یک WCF Service قرار گرفته روی یک سرور مجزا هم می‌تونه از DataLayer و ServiceLayer مثال جاری استفاده کند. استفاده کننده نهایی برای نمایش آن در UI با هیچکدام از دو مورد ذکر شده کاری ندارد و فقط با قراردادهای WCF Service کار می‌کنه.
نظرات مطالب
واکشی اطلاعات سرویس Web Api با استفاده از TypeScript و AngularJs
httpBackend$ یک پیاده سازی fake از http$ است، در نتیجه می‌توانید در هنگام تست، این سرویس را به کنترلرهای خود تزریق کنید. اما قبل از DI باید برای این سرویس مشخص شود که برای مثال در هنگام مواجه شدن با یک درخواست از نوع Get و آدرس X چه خروجی برگشت داده شود. درست شبیه به رفتار mocking framework ها. فرض کنید شما کنترلری به شکل زیر دارید:
(function (module) {
 
    var myController = function ($scope, $http) {
 
        $http.get("/api/myData")
            .then(function (result) {
                $scope.data= result.data;
            });
    };
 
    module.controller("MyController",
        ["$scope", "$http", myController]);
 
}(angular.module("myApp")));
همان طور که می‌بینید در این کنترلر از http$ استفاده شده است. حال برای تست آن می‌توان نوشت:
describe("myApp", function () {
 
    beforeEach(module('myApp'));
 
    describe("MyController", function () {
 
        var scope, httpBackend;
        beforeEach(inject(function ($rootScope, $controller, $httpBackend, $http) {
            scope = $rootScope.$new();
            httpBackend = $httpBackend;
            httpBackend.when("GET", "/api/myData").respond([{}, {}, {}]);
            $controller('MyController', {
                $scope: scope,
                $http: $http
            });
        }));
 
        it("should have 3 row", function () {
            httpBackend.flush();
            expect(scope.data.length).toBe(3);
        });
    });
});
httpBackend ساخته شده با استفاده از سرویس controller$ به کنترلر مورد نظر تزریق می‌شود. حال اگر در یک کنترلر 5 بار از سرویس http$ برای فراخوانی 5 resource متفاوت استفاده شده باشد باید برای هر حالت httpBackend$ را طوری تنظیم کرد که بداند برای هر منبع چه خروجی در اختیار کنترلر قرار دهد.


مطالب
راه اندازی Docker swarm
مقاله‌های زیادی درباره‌ی مزایای استفاده‌ی از داکر در اینترنت وجود دارند. در این مقاله قصد دارم طریقه‌ی راه اندازی یک سرور Production را برای داکر، توضیح دهم.
یکی از مزایای مهم داکر، امکان Scale در سریعترین زمان ممکن هست. یعنی اگر در محیط Production میزان بار بر روی یکی از اجزای محصول شما بیشتر بود (در صورتیکه معماری صحیحی برای سرویس‌های مجزا رعایت شده باشد)، می‌توانید آن قسمت را Scale کنید. می‌دانید که وجود بیشتر از یک Instance از یک سرویس، نگرانی‌های معمولی مثل Load Balancing  و غیره را به همراه دارد و اینکه کانتینر داکر شما قرار هست بر روی چه سروری نصب شود و بر روی چه سرور یا سرور‌هایی Scale شود.
ابزار‌های خوبی برای مدیریت کردن سرورهایی که Container‌های داکر بر روی آنها اجرا می‌شوند وجود دارند که یکی از مهمترین آنها kubernetes است که یکی از بهترین ابزارها، برای Management، Scaling و Deploy اتوماتیک نرم افزارهای Containerized می‌باشد.
در این مقاله از ابزاری به نام Swarm استفاده می‌کنم که راه اندازی آن راحت‌تر هست و همینطور وقتی احتیاج به Scaling وجود داشت، فقط یک PublishedPort به سرویس اختصاص داده می‌شود و  Load Balancing کاملا اتوماتیک انجام خواهد شد.
برای نصب Swarm، من دو سرور لینوکسی را آماده می‌کنم. یکی از سرور‌ها نقش Master را ایفا می‌کند و دیگری نقش Worker را (به راحتی می‌شود تعداد Worker‌ها را افزایش داد). 
از مزایای Swarm هم می‌شود به این نکته اشاره کرد که وقتی به سخت افزار بیشتری احتیاج باشد، کافی است یک ماشین دیگر را راه اندازی و آن را به عنوان یک Worker به Master معرفی کنیم و لازم نیست نگرانی درباره‌ی اینکه چه کانتینری بر روی چه سروری اجرا می‌شود داشته باشیم.
مشخصات سرور‌ها:
DocerMaster :
OS : CentOS7
IP: 192.168.64.3

DockerWorker:
OS: CentOS7
IP: 192.168.64.4
مطمئن شوید که هر دو در یک شبکه هستند و همدیگر را پینگ می‌کنند.
سپس بر روی هر دو سرور، داکر را نصب می‌کنم:
sudo yum install docker
و در ادامه سرویس داکر را  بر روی هر دو سیستم استارت می‌کنیم: 
$ sudo service docker start
$ sudo systemctl start docker.service
 مطمئن شوید که داکر در هر دو سرور نصب و اجرا شده‌است. با دستور service status docker می‌توانید نتیجه را ببینید.
بعد از نصب داکر، به صورت پیشفرض Swarm هم نصب می‌شود و لازم به نصب ابزار دیگری نیست.
برای ارتباط بین Master و Worker‌ها باید بعضی از پورت‌ها در این سرور‌ها باز شود. برای این کار در کامپیوتر Master دستورات زیر را اجرا کنید تا پورت‌ها به فایروال اضافه شوند: 
firewall-cmd --permanent --add-port=2376/tcp
firewall-cmd --permanent --add-port=2377/tcp
firewall-cmd --permanent --add-port=7946/tcp
firewall-cmd --permanent --add-port=7946/udp
firewall-cmd --permanent --add-port=4789/udp
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --reload
و سپس دستور زیر را اجرا کنید: 
systemctl restart docker
به کامپیوتر Worker رفته و با دستورات زیر پورت‌های لازم را به فایروال اضافه کنید: 
~]# firewall-cmd --permanent --add-port=2376/tcp
~]# firewall-cmd --permanent  --add-port=7946/tcp
~]# firewall-cmd --permanent --add-port=7946/udp
~]#  firewall-cmd --permanent --add-port=4789/udp
~]# firewall-cmd --permanent --add-port=80/tcp
~]#  firewall-cmd --reload
~]#  systemctl restart docker
در سرور Master باید Swarm را راه اندازی کنیم. برای این کار از دستور زیر استفاده می‌کنیم: 
sudo docker swarm init –advertise-addr 192.168.64.3
 بعد از اجرای موفقیت آمیز دستور فوق، عبارت زیر نمایش داده می‌شود: 

همانطور که مشاهده می‌کنید، پس از راه اندازی، اعلانی مبنی بر اینکه این نود به عنوان Manager شناخته شده و اینکه برای اضافه کردن یک نود Worker چه دستوری را باید اجرا کرد، نمایش داده شده‌است.

اکنون کافی‌است این خط کد را در نود Worker کپی کنیم: 

بعد از موفقیت آمیز بودن اجرای آن، می‌توانید در کامپیوتر Master، با دستور زیر تمام نود‌ها را مشاهده کنید: 

$ sudo docker node ls

همانطور که مشاهده می‌کنید، دو نود وجود دارد که یکی به عنوان Leader شناخته می‌شود. هر زمانی که نیاز بود، می‌شود به راحتی یک Worker دیگر را اضافه کرد.

برای راه اندازی یک کانتینر، swarm از CLI کاملی برخوردار هست؛ اما مایلم اینجا از یک ابزار خوب، برای مدیریت Swarm استفاده کنم. Portainer به عنوان یه ابزار عالی برای مدیریت Image‌ها و Container‌های داکر محسوب می‌شود که کاملا swarm را پشتیبانی می‌کند.

برای راه اندازی portainer کافی است کد زیر را در سیستم Master اجرا کنید: 

$ docker volume create portainer_data
$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer

البته به دلیل عدم دسترسی به داکر هاب از کشور ایران، عملا امکان pull کردن این image، مستقیما از داکر هاب و بدون وی پی ان وجود ندارد. 

بعد از موفقیت آمیز بودن راه اندازی portainer می‌توانید از طریق آدرس http://192.168.64.3:9000 به آن دسترسی داشته باشید. در اولین ورود، پسورد ادمین را تنظیم می‌کنید و بعد از وارد شدن، صفحه‌ای مطابق شکل زیر را خواهید دید:  

اگر بر روی منوی swarm کلیک کنید، همه‌ی نود‌ها را مشاهده خواهید کرد و در صورتیکه بر روی Containers کلیک کنید، همه‌ی Container هایی را که بر روی این سرور وجود دارند، خواهید دید. مهمترین قسمت، بخش Service هاست که مشخصات Container هایی که روی swarm توزیع شدن را نشان می‌دهد و همینطور تعداد Container هایی از این image که Scale شدند. همینطور که می‌بینید فعلا فقط همین Portainer در حال اجراست. 


اجازه دهید یک مثال کاربردی‌تر بزنیم و یک سرویس را ایجاد کنیم.

من بر روی کامپیوتر شخصی‌ام و نه سرورها، با دستور زیر یک پروژه‌ی MVC را با دات نت Core ایجاد می‌کنم:

dotnet new mvc

و سپس دستور dotnet publish را اجرا می‌کنم و به پوشه‌ای که محتویات پابلیش شده در آن قرار دارند رفته و یک فایل بدون پسوند را به نام dockerfile ایجاد می‌کنم و متن زیر را در آن می‌نویسم:

همینطور که می‌بینید من از image مخصوص اجرای دات نت Core در این container استفاده می‌کنم. پوشه‌ی کانتینر را تنظیم می‌کنم و همه‌ی فایل‌هایی که در پوشه‌ی جاری سیستم خودم وجود دارند را به پوشه‌ی جاری کانتینر منتقل می‌کنم و سپس دستور دات نت را با پارامتر اسم dll پروژه‌ام اجرا می‌کنم. این کل محتویات فایل داکر من هست.

ترمینال را در همین پوشه‌ی publish باز می‌کنم و دستور زیر را اجرا می‌کنم:

docker build –t swarmtest:dev .
با این دستور یک image از روی داکر فایلی که ایجاد کردیم درست می‌شود:

حالا باید این image را به سرور master منتقل کنیم. برای این کار راه‌های مختلفی وجود دارند که معمول‌ترین آن‌ها push کردن این image به یک registery و سپس pull کردن آن در کامپیوتر Master است که من از این راه استفاده نمی‌کنم. من این image را بر روی کامپیوترم ذخیره می‌کنم و سپس فایل ذخیره شده را به سرور master منتقل می‌کنم و آنجا آن فایل را load می‌کنم.
در ادامه بر روی کامپیوتر خودم دستور زیر را اجرا می‌کنم:
docker save swarmtest:dev –o swarmtest.tar

طبق شکل زیر یک فایل tar که حاوی image برنامه من هست، ایجاد شد:

حالا با دستور زیر این فایل رو به سرور Master منتقل می‌کنم:

scp –r swarmtest.tar root@192.168.64.3:/srv/images

همانطور که می‌بینید، فایل tar به پوشه‌ای که قبلا در سرور ایجاد کردم، منتقل شد.

حالا به سرور و پوشه‌ای که فایل tar آنجا قرار دارد رفته و با دستور زیر این image را بر روی سیستم load می‌کنم:

sudo docker load –i swarmtest.tar

همانطور که در تصویر می‌بینید، بعد از load شدن، image مورد نظرمان به داکر اضافه شده‌است.

حالا برای اجرا کردن این سرویس بر روی swarm، آدرس portainer را باز می‌کنیم و به قسمت  services می‌رویم و بر روی دکمه‌ی add service کلیک می‌کنیم:

در قسمت نام، نام سرویس و در قسمت imageConfiguration از منوی image‌ها، ایمیجی را که ایجاد کردیم، انتخاب می‌کنیم. در قسمت Replicas تعداد instance‌های container ای را که می‌خواهیم از روی image ایجاد شوند، مشخص می‌کنیم. (این قسمت را بر روی هر وضعیتی می‌توانیم قرار دهیم و زیاد و کم کنیم) و در قسمت port mapping، پورت درون Container و پورتی را که می‌خواهیم بر روی هاست به نمایش درآید، وارد می‌کنیم.

همانطور که می‌بینید من به راحتی می‌توانم تعداد Container‌ها را Scale کنم و نگرانی‌ای بابت load balancing و اینکه کدام container بر روی کدوم سرور ایجاد می‌شود، نخواهم داشت.

برای نمایش برنامه کافی است پورتی را که برای هاست وارد کردیم، با آی پی Master وارد کنیم:


مطالب
پیاده سازی لایه دسترسی به داده ها توسط EF CodeFirst و Service Layer
چندی پیش یک مجموعه آموزشی کامل تحت عنوان EF CodeFirst توسط آقای نصیری در این سایت قرار داده شد که بسیار کامل و زیبا بود.یک پیاده سازی بر اساس این آموزش‌ها تهیه کردم که می‌توانید از اینجا دریافت نمایید و شامل پروژه‌های زیر می‌باشد:  EFCodeFirst-GenericServices.rar

DomainClasses : شامل کلاس‌های مربوطه جهت نگاشت به جداول پایگاه داده ؛ به علاوه کانفیگ‌های مربوطه می‌باشد.
DataLayer : لایه دسترسی به داده‌ها می‌باشد که شامل اینترفیس IUnitOfWork و یک پیاده سازی از آن در شئی Context می‌باشد.
Service Layer : شامل اینترفیس‌ها و کلاس‌های لایه سرویس می‌باشد.ابتدا اینترفیس‌های مربوطه نوشته شده و سپس پیاده سازی مربوط EF آن در یک پوشه دیگر انجام شده است.لازم به ذکر است که دستورات مربوط به کار با EF به علاوه منطق تجاری برنامه در این لایه قرار می‌گیرند.
CommonLib : یک پروژه جهت نگهداری متدهای عمومی و Helper می‌باشد که اینجا مطلب خاصی ندارد و فقط شامل دو پیاده سازی مربوط به تاریخ شمسی می‌باشد که مهم نیستند! از این پروژه در Domain Class و Data Layer جهت تبدیل تاریخ میلادی به شمسی استفاده شده که می‌شد این کار را با کلاس‌های داخلی دات نت نیز انجام داد و این پروژه را حذف نمود.

تنها تفاوت این پیاده سازی با مطالب موجود در سایت، Generic بودن اینترفیس‌ها و کلاس‌های لایه Service می‌باشد که میزان کد نویسی را کاهش داده است.
 
مطالب
تست کردن متدهای یک Controller به کمک PowerShell

ابزارهای زیادی جهت تست کردن API ‌های برنامه‌های وب موجود است. یکی از معروفترین آنها  Fiddler است که ابزاری مستقل جهت دیباگ تحت پروتکل HTTP می‌باشد. یکی دیگر از این نرم افزارها Swashbuckle است که از Nuget قابل دریافت است و صفحه‌ای را به برنامه اضافه می‌کند که به اختصار API ‌های برنامه را نمایش داده و امکان اجرای آنها را فراهم میکند.

اما ساده‌ترین راه جهت کردن تست کردن API ‌های یک برنامه، استفاده از PowerShell است که عمل ایجاد درخواست‌های HTTP را به راحتی از طریق Command line فراهم میکند و به شما اجازه میدهد بدون وارد شدن به جزئیات، بر روی نتیجه API مورد نظر تمرکز کنید. PowerShell ابتدا فقط برای محیط ویندوز طراحی شده بود ولی در حال حاضر قابلیت اجرای تحت Linux و Mac را نیز دارد.

در این بخش به شما نشان خواهم داد که چگونه متدهای یک Controller را با استفاده از PowerShell تست نمایید.

بدین منظور ابتدا کلاسی را به نام Reservation با مشخصات زیر، در یک پروژه ASP.NET Core Web Application  ایجاد نمایید.
public class Reservation
{
        public int ReservationId { get; set; }
        public string ClientName { get; set; }
        public string Location { get; set; }
}
و سپس Interface زیر را جهت انجام عملیات CRUD اضافه نمائید:
public interface IRepository
{
        IEnumerable<Reservation> Reservations { get; }
        Reservation this[int id] { get; }
        Reservation AddReservation(Reservation reservation);
        Reservation UpdateReservation(Reservation reservation);
        void DeleteReservation(int id);
}
و کلاسی که Interface فوق را پیاده سازی خواهد کرد:
public class MemoryRepository : IRepository
 {
        private Dictionary<int, Reservation> items;
        public MemoryRepository()
        {
            items = new Dictionary<int, Reservation>();
            new List<Reservation> {
                new Reservation { ClientName = "Alice", Location = "Board Room" },
                new Reservation { ClientName = "Bob", Location = "Lecture Hall" },
                new Reservation { ClientName = "Joe", Location = "Meeting Room 1" }
            }.ForEach(r => AddReservation(r));
        }

        public Reservation this[int id] => items.ContainsKey(id) ? items[id] : null;
        public IEnumerable<Reservation> Reservations => items.Values;

        public Reservation AddReservation(Reservation reservation)
        {
            if (reservation.ReservationId == 0)
            {
                int key = items.Count;
                while (items.ContainsKey(key)) { key++; };
                reservation.ReservationId = key;
            }
            items[reservation.ReservationId] = reservation;
            return reservation;
        }

        public void DeleteReservation(int id) => items.Remove(id);
        public Reservation UpdateReservation(Reservation reservation) => AddReservation(reservation);
}
در کلاس فوق به جهت سهولت در کار، از یک لیست، برای نگهداری تعدادی رکورد اطلاعاتی جهت نمایش استفاده شده است.

سپس کنترلری را به نام ReservationController به پروژه اضافه کنید که شامل متدهای زیر باشد:
[Route("api/[controller]")]
public class ReservationController : Controller
    {
        private IRepository repository;
        public ReservationController(IRepository repo) => repository = repo;

        [HttpGet]
        public IEnumerable<Reservation> Get() => repository.Reservations;

        [HttpGet("{id}")]
        public Reservation Get(int id) => repository[id];

        [HttpPost]
        public Reservation Post([FromBody] Reservation res) =>
                    repository.AddReservation(new Reservation
                    {
                        ClientName = res.ClientName,
                        Location = res.Location
                    });
        [HttpPut]
        public Reservation Put([FromBody] Reservation res) => repository.UpdateReservation(res);

        [HttpPatch("{id}")]
        public StatusCodeResult Patch(int id, [FromBody] JsonPatchDocument<Reservation> patch)
        {
            Reservation res = Get(id);
            if (res != null)
            {
                patch.ApplyTo(res);
                return Ok();
            }
            return NotFound();
        }

        [HttpDelete("{id}")]
        public void Delete(int id) => repository.DeleteReservation(id);
}
حالا جهت تست صحت این API‌ها توسط PowerShell لازم است ابتدا برنامه را اجرا کرده و منتظر بمانید تا صفحه Home ظاهر شود. پس از آن لازم است یک پنجره PowerShell را از طریق کلیک راست در یکی از فولدرهای نمایش داده شده در FileExplorer باز کنید.

تست کردن درخواست از نوع GET

حالا دستور زیر را در پنجره PowerShell وارد کنید:
 Invoke-RestMethod http://localhost:7000/api/reservation -Method GET
لازم است شماره پورت را مطابق با پورتی که برنامه شما بر روی آن اجرا شده تغییر دهید. این فرمان به کمک دستور Invoke-RestMethod درخواستی از نوع GET را به api/reservation ارسال می‌کند و نتیجه را پس از تجزیه و تحلیل و فرمت بندی، جهت سهولت در خوانده شدن، به شکل زیر نمایش میدهد:

location  clientName  reservationId 
Board Room 
Alice 
0                  
Lecture Hall 
Bob
1
Meeting Room 1 
Joe
2
فرمت اطلاعات دریافت شده از درخواست GET، اشیایی از نوع کلاس Reservation است و به فرمت Json می‌باشد؛ ولی Invoke-RestMethod آنرا به صورت جدول نمایش میدهد.
حال جهت نمایش فقط یک رکورد از اطلاعات فوق، دستور زیر را وارد نمایید:
Invoke-RestMethod http://localhost:7000/api/reservation/1 -Method GET
این فرمان درخواستی جهت دریافت اطلاعات شیء Reservation است که کد داخلی آن برابر 1 است. پاسخ این درخواست به شکل زیر نمایش داده خواهد شد:

location  clientName   reservationId   
Lecture Hall  
        Bob
1                  


تست درخواست از نوع Post

تمامی متدهای ارائه شده توسط یک API را می‌توان به کمک PowerShell تست کرد. اگرچه در این حالت فرمت بعضی از درخواستهای ارسالی ممکن است کمی به هم ریخته به نظر برسد. در اینجا درخواستی جهت اضافه کردن یک رکورد را به لیست Reservation ارسال می‌کنیم و نتیجه را در پاسخ ارسال شده از سمت سرور خواهیم دید:

Invoke-RestMethod http://localhost:7000/api/reservation -Method POST -Body
(@{clientName="Anne"; location="Meeting Room 4"} | ConvertTo-Json) -ContentType "application/json"
این دستور با استفاده از آرگومان Body- محتویات درخواست را که با فرمت Json است تعیین می‌کند. آرگومان Content-Type- هم نوع محتویات درخواستی را در هدر مشخص میکند. دستور فوق در صورت موفقیت نتیجه زیر را نمایش خواهد داد:

location  clientName   reservationId   
 Meeting Room 4
Anne      
1                
           
دستور Post ارسالی، با استفاده از دو فیلد clientName و location، یک رکورد جدید از نوع Reservation را به لیست موجود اضافه کرده و نتیجه را در قالب Json به سمت کلاینت برگشت خواهد داد که شامل ReservationId می‌باشد که به رکورد جدید اختصاص داده شده‌است. ممکن است چنین به نظر برسد که نتیجه برگشت داده شده، همان درخواست ارسالی است که به شکل جدول فوق نمایش داده می‌شود؛ ولی چنین نیست. جهت مشاهده تاثیر درخواست POST ارسالی بر روی منبع داده، کافی است یک بار دیگر دستور زیر را در command line وارد کنید:
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

location  clientName   reservationId   
 Board Room           Alice
0              
 Lecture Hall  Bob 1
 Meeting Room 1  Joe 2
 Meeting Room 4  Anne 3
داده‌هایی که به سمت کلاینت ارسال شده، نشان دهنده اضافه شدن رکورد جدیدی به منبع داده ماست.

تست درخواست از نوع PUT

درخواست از نوع PUT جهت جایگزینی داده‌ای موجود در لیست، با داده‌ی جدید است. بدین منظور کد داخلی شیء مورد نظر (در اینجا ReservationId) باید در URL گنجانده شود و مقادیر فیلدهای کلاس، در متن درخواست. مثالی جهت درخواست اصلاح داده موجود از طریق فرمان PowerShell :

Invoke-RestMethod http://localhost:7000/api/reservation -Method PUT -Body
(@{reservationId="1"; clientName="Bob"; location="Media Room"} | ConvertTo-Json)
-ContentType "application/json"
درخواست فوق، فیلد Location رکوردی با کد داخلی 1 را به مقدار جدیدی تغییر خواهد داد و نتیجه تغییر انجام شده را در پاسخ ارسال خواهد کرد:

location  clientName   reservationId   
Media Room
Bob        
1                

و باز با ارسال درخواست زیر می‌توان نتیجه کلی را مشاهده کرد:
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

location  clientName   reservationId   
Board Room
Alice  
0  
 Media Room
 Bob  1
 Meeting Room 1
Joe 2
 Meeting Room 4
Anne
3


استفاده از دستور PATCH

این دستور جهت تغییر اطلاعات یک رکورد به کار میرود، ولی استفاده از آن در غالب برنامه‌های پیاده سازی شده نادیده گرفته می‌شود که البته چنانچه کاربران برنامه‌های مذکور به تمامی فیلدهای یک رکورد دسترسی داشته باشند، معقولانه به نظر می‌رسد. اما در یک برنامه بزرگ و پیچیده ممکن است به دلایلی از جمله مسایل امنیتی، کاربران اجازه دسترسی به تمامی فیلدهای یک رکورد را نداشته باشند و در این حالت نمی‌توان از دستور PUT جهت ارسال درخواست استفاده کرد. دستورهای مبتنی بر PATCH گزینشی بوده و می‌توان فقط فیلدهای خاصی را که مورد نظر می‌باشند، با آن تغییر داد.

ASP.NET Core MVC از استاندارد Json PATCH پشتیبانی می‌کند و این کار اجازه میدهد تا بتوان تغییرات را بطور یکنواختی انجام داد. جهت مشاهده جزئیات این دستور می‌توانید به این لینک مراجعه کنید. اما برای مثال فرض کنید می‌خواهید چنین درخواستی را که به فرمت Json است از طریق HTTP PATCH به سمت سرور ارسال کنید:

[
  { "op": "replace", "path": "clientName", "value": "Bob"},
  { "op": "replace", "path": "location", "value": "Lecture Hall"}
]

دربسیاری از مواقع فقط از دستور replace درخواست PATCH استفاده می‌شود و کد داخلی رکورد مورد نظر، جهت تغییر در Url درخواست ارسالی، فرستاده خواهد شد. ASP.NET Core MVC به طور اتوماتیک این دستور را پردازش کرده و آنرا به صورت آبجکتی از نوع <JsonPatchDocument<T به اکشن کنترلر قید شده، پاس خواهد داد که در اینجا نوع T، از نوع کلاس مورد نظر ما می‌باشد. فرمت دستور ارسالی از طریق Powershell به شکل زیر خواهد بود:

Invoke-RestMethod http://localhost:7000/api/reservation/2 -Method PATCH -Body (@
{ op="replace"; path="clientName"; value="Bob"},@{ op="replace"; path="location";
value="Lecture Hall"} | ConvertTo-Json) -ContentType "application/json"
دستور فوق درخواست تغییر دو فیلد clientname و location، با کد داخلی 2 می‌باشد.
جهت مشاهده تغییر ایجاد شده، دستور زیر را مجددا اجرا نمایید:
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

location  clientName   reservationId  
Board Room
Alice  
0            
Media Room
Bob
 1
Lecture Hall
 Bob 2
Meeting Room 4
 Anne 3


استفاده از دستور Delete

استفاده از دستور Delete هم به شکل زیر خواهد بود:

Invoke-RestMethod http://localhost:7000/api/reservation/2 -Method DELETE
توضیح اینکه در این حالت پاسخی از سمت سرور ارسال نخواهد شد. اما جهت مشاهده تاثیر این دستور بر روی اطلاعات موجود می‌توان مجددا دستور زیر را جهت نمایش داده‌ها بکار برد:
Invoke-RestMethod http://localhost:7000/api/reservation -Method GET

کدهای این مقاله به پیوست موجود است: ApiControllers.zip
مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت دوم - الگوی Service Locator
در قسمت قبل برای دریافت وهله‌ای از سرویس TestService، به صورت ()<serviceProvider.GetService<ITestService عمل کردیم. این روش در اصل الگوی Service Locator نام دارد که جزئیات بیشتری از آن‌را در این قسمت بررسی خواهیم کرد.


قلب سیستم تزریق وابستگی‌های NET Core. اینترفیس IServiceProvider است

IServiceProvider که اساس IoC Container برنامه‌های مبتنی بر NET Core. را تشکیل می‌دهد، در اسمبلی System.ComponentModel و در فضای نام System تعریف شده‌است:
namespace System
{
    public interface IServiceProvider
    {
        object GetService(Type serviceType);
    }
}
زمانیکه به کمک IServiceCollection، تمام اینترفیس‌ها و کلاس‌های خود را به IoC Container معرفی کردیم، مرحله‌ی بعدی، فراهم آوردن روشی برای دریافت وهله‌ای از این سرویس‌ها توسط متد GetService است.
استفاده‌ی مستقیم از اینترفیس IServiceProvider برای دسترسی به وهله‌های سرویس‌ها، اصطلاحا الگوی Service Locator نامیده می‌شود و باید تا حد ممکن از آن پرهیز کرد؛ چون وابستگی مستقیمی از IoC Container را درون کدهای ما قرار می‌دهد و به این ترتیب یک مرحله، نوشتن آزمون‌های واحد برای آن‌را مشکل‌تر می‌کند؛ چون زمان وهله سازی از یک سرویس، دقیقا مشخص نیست به چه وابستگی‌هایی نیاز دارد. به همین جهت همیشه باید با روش تزریق وابستگی‌ها در سازنده‌ی کلاس شروع کرد و اگر به هر دلیلی این روش مهیا نبود و توسط سیستم تزریق وابستگی‌های جاری شناسایی و یا پشتیبانی نمی‌شد (مانند تزریق وابستگی در سازنده‌های Attributes)، آنگاه می‌توان به الگوی Service Locator مراجعه کرد.
برای مثال در اکثر قسمت‌های برنامه‌های ASP.NET Core امکان تزریق وابستگی‌ها در سازنده‌ی کنترلرها، میان افزارها و سایر اجزای آن وجود دارد و در این حالات نیازی به مراجعه‌ی مستقیم به IServiceProvider برای دریافت وهله‌های سرویس‌های مورد نیاز نیست. به عبارتی نگرانی در مورد IServiceProvider بهتر است مشکل IoC Container باشد و نه ما.

در مثال زیر، روش استفاده‌ی از IServiceProvider را جهت انجام تزریق وابستگی‌ها (یا به عبارتی بهتر، روش دسترسی به وهله‌های وابستگی‌ها) را مشاهده می‌کنید:
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace CoreIocServices
{
    public interface IProductService
    {
        void Delete(int id);
    }

    public class ProductService : IProductService
    {
        private readonly ITestService _testService;
        private readonly ILogger<ProductService> _logger;

        public ProductService(IServiceProvider serviceProvider)
        {
            _testService = serviceProvider.GetRequiredService<ITestService>();
            _logger = serviceProvider.GetService<ILogger<ProductService>>() ?? NullLogger<ProductService>.Instance;
        }

        public void Delete(int id)
        {
            _testService.Run();
            _logger.LogInformation($"Deleted a product with id = {id}");
        }
    }
}
این روش یا کار مستقیم با Service locator، هر چند کار می‌کند، اما روشی است که باید تا حد ممکن از آن پرهیز کنید؛ زیرا:
- با نگاه کردن به امضای سازنده‌ی این سرویس مشخص نیست که دقیقا از چه وابستگی‌هایی استفاده می‌کند. اینکار نوشتن آزمون‌های واحد آن‌را مشکل می‌کند.
- این سرویس یک وابستگی اضافه‌تر را به نام IServiceProvider، نیز پیدا کرده‌است که اگر از روش متداول تزریق وابستگی‌ها در سازنده‌ی کلاس استفاده می‌شد، نیازی به ذکر آن نبود.
- پیاده سازی Dispose Pattern در این حالت مشکل‌تر است و در قسمتی دیگر بررسی خواهد شد.



تفاوت‌های بین متدهای ()<GetService<T  و  ()<GetRequiredService<T

از آنجائیکه دیگر از NET 1.0. استفاده نمی‌کنیم، استفاده‌ی از متد GetService با امضایی که در اینترفیس IServiceProvider تعریف شده و strongly typed نیست، بیشتر برای کارهای پویا مناسب است. به همین جهت دو نگارش جنریک از آن در اسمبلی Microsoft.Extensions.DependencyInjection.Abstractions با امضای زیر تعریف شده‌اند که نمونه‌ای از آن‌را در قسمت قبل نیز استفاده کردیم و برای استفاده‌ی از آن‌ها ذکر فضای نام Microsoft.Extensions.DependencyInjection ضروری است:
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceProviderServiceExtensions
    {
        public static T GetRequiredService<T>(this IServiceProvider provider);
        public static T GetService<T>(this IServiceProvider provider);
    }
}
اکنون این سؤال مطرح می‌شود که تفاوت‌های بین این دو متد چیست؟
- متد GetService یک شیء سرویس از نوع T را بازگشت می‌دهد و یا نال؛ اگر سرویسی از نوع T، پیشتر به سیستم معرفی نشده باشد.
- متد GetRequiredService یک شیء سرویس از نوع T را بازگشت می‌دهد و یا اگر سرویسی از نوع T پیشتر به سیستم معرفی نشده باشد، استثنای InvalidOperationException را صادر می‌کند.

بنابراین تنها تفاوت این دو متد، در نحوه‌ی رفتار آن‌ها با درخواست وهله‌ای از یک سرویس پیشتر ثبت نشده‌است؛ یکی نال را باز می‌گرداند و دیگری یک استثناء را صادر می‌کند.


با توجه به این تفاوت‌ها کدامیک از متدهای GetService و یا GetRequiredService را باید استفاده کرد؟

همانطور که پیشتر نیز در توضیحات الگوی Service locator عنوان شد، هیچکدام! ابتدا با تزریق وابستگی‌های در سازنده‌ی کلاس شروع کنید و اگر تامین این وابستگی، توسط IoC Container جاری پشتیبانی نمی‌شد، آنگاه نیاز به استفاده‌ی از یکی از نگارش‌های متد GetService خواهد بود و متد توصیه شده نیز GetRequiredService است و نه GetService؛ به این دلایل:
- حذف کدهای تکراری: اگر از GetService استفاده کنید، نیاز خواهید داشت پس از تمام فراخوانی‌های آن، بررسی نال بودن آن‌را نیز انجام دهید. برای حذف این نوع کدهای تکراری، بهتر است از همان متد GetRequiredService استفاده کنید که به صورت توکار این بررسی را نیز انجام می‌دهد.
- پشتیبانی از روش Fail Fast و یا همان Defensive programming: اگر بررسی نال بودن GetService را فراموش کنید، در سطرهای بعدی، یافتن علت NullReferenceException صادر شده مشکل‌تر از رسیدگی به InvalidOperationException صادر شده‌ی توسط GetRequiredService خواهد بود که توضیحات دقیقی را در مورد سرویس ثبت نشده ارائه می‌دهد.
- اگر بر روی IoC Container پیش‌فرض NET Core. یک IoC Container دیگر را مانند AutoFac قرار داده‌اید، استفاده‌ی از GetRequiredService، سبب می‌شود تا اینگونه IoC Containerهای ثالث بتوانند اطلاعات مفیدتری را از سرویس‌های ثبت نشده ارائه دهند.

تنها حالتی که استفاده‌ی از روش GetService را نیاز دارد، شرطی کردن ثبت و معرفی کردن سرویس‌ها به IoC Container است؛ اگر سرویسی ثبت شده بود، آنگاه قطعه کدی اجرا شود.
مطالب دوره‌ها
مثال - نمایش درصد پیشرفت عملیات توسط SignalR
برنامه‌های وب در سناریوهای بسیاری نیاز دارند تا درصد پیشرفت عملیاتی را به کاربران گزارش دهند. نمونه ساده آن، گزارش درصد پیشرفت میزان دریافت یک فایل است و یا اعلام درصد انجام یک عملیات طولانی از سمت سرور به کاربر. در ادامه قصد داریم این موضوع را توسط SignalR پیاده سازی کنیم.


نکته‌ای در مورد نگارش‌های مختلف SignalR
اگر برنامه شما قرار است دات نت 4 را پشتیبانی کند، آخرین نگارش SignalR که با آن سازگار است، نگارش 1.1.3 می‌باشد. بنابراین اگر دستور ذیل را اجرا کنید:
 PM> Install-Package Microsoft.AspNet.SignalR
SignalR 2 را نصب می‌کند که با دات نت 4 و نیم به بعد سازگار است.
اگر دستور ذیل را اجرا کنید، SiganlR 1.x را نصب می‌کند که با دات نت 4 به بعد سازگار است:
 PM> Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
پیش فرض این مطلب نیز استفاده از نگارش 1.1.3 می‌باشد تا بازه بیشتری از وب سرورها را شامل شود.
با اینکار Microsoft.AspNet.SignalR.JS نیز به صورت خودکار نصب می‌گردد و به این ترتیب کلاینت جاوا اسکریپتی SiganlR نیز در برنامه قابل استفاده خواهد بود.


تنظیمات فایل Global.asax.cs

سطر فراخوانی متد RouteTable.Routes.MapHubs باید در ابتدای متد Application_Start فایل Global.asax.cs قرار گیرد (پیش از هر تنظیم دیگری). تفاوتی هم نمی‌کند که برنامه وب فرم است یا MVC. به این ترتیب مسیریابی‌های SignalR تنظیم شده و مسیر http://localhost/signalr/hubs قابل استفاده خواهد بود.


تنظیمات اسکریپت‌های سمت کلاینت مورد نیاز

پس از نصب بسته SignalR، سه اسکریپت ذیل باید به ابتدای صفحه وب اضافه شوند تا کلاینت‌های جاوا اسکریپتی SignalR بتوانند با سرور ارتباط برقرار کنند:
 <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="Scripts/jquery.signalR-1.1.3.min.js" type="text/javascript"></script>
<script src="signalr/hubs" type="text/javascript"></script>
این تنظیمات نیز برای هر دو نوع برنامه‌های وب فرم و MVC یکسان است.


تعریف کلاس Hub برنامه

using Microsoft.AspNet.SignalR;

namespace WebFormsSample03.Common
{
    public class ProgressHub : Hub
    {
        /// <summary>
        /// این متد استاتیک تعریف شده تا در برنامه به صورت مستقیم قابل استفاده باشد
        /// یا می‌شد اصلا این متد تعریف نشود و از همان دریافت زمینه هاب در کنترلر استفاده گردد
        /// </summary>        
        public static void UpdateProgressBar(int value, string connectionId)
        {
            var ctx = GlobalHost.ConnectionManager.GetHubContext<ProgressHub>();
            ctx.Clients.Client(connectionId).updateProgressBar(value); //فراخوانی یک متد در سمت کلاینت
        }
    }
}
متدی که در کلاس هاب برنامه تعریف شده، از نوع استاتیک است. از این جهت که می‌خواهیم این متد را در خارج از این هاب و در یک کنترلر Web API فراخوانی کنیم. زمانیکه متدی به صورت استاتیک تعریف می‌شود، ارتباط آن با وهله جاری کلاس یا this قطع خواهد شد. به همین جهت نیاز است تا از طریق متد GlobalHost.ConnectionManager.GetHubContext مجددا به context کلاس هاب دسترسی پیدا کنیم.
البته تعریف این متد در اینجا ضروری نبود. حتی می‌شد بدنه کلاس هاب را خالی تعریف کرد و متد GetHubContext را مستقیما داخل یک کنترلر فراخوانی نمود.
متد UpdateProgressBar، مقدار value را به تنها یک کلاینت که Id آن مساوی connectionId دریافتی است، ارسال می‌کند. این کلاینت باید یک callback جاوا اسکریپتی را جهت تامین متد پویای updateProgressBar تدارک ببیند.


کلاس Web API کنترلر دریافت فایل‌ها

فرقی نمی‌کند که برنامه شما از نوع وب فرم است یا MVC. امکانات Web API در هر دو نوع پروژه، قابل دسترسی است (همان ایده یک ASP.NET واحد).
بنابراین نیاز است یک کنترلر وب API جدید را به پروژه اضافه کرده و محتوای آن را به شکل ذیل تغییر دهیم:
using System.Threading;
using System.Web.Http;
using WebFormsSample03.Common;

namespace WebFormsSample03
{
    public class DownloadRequest
    {
        public string Url { set; get; }
        public string ConnectionId { set; get; }
    }

    public class DownloaderController : ApiController
    {
        public void Post([FromBody]DownloadRequest data)
        {
            //todo: start downloading the data.Url ....

            ProgressHub.UpdateProgressBar(10, data.ConnectionId);
            Thread.Sleep(2000);

            ProgressHub.UpdateProgressBar(40, data.ConnectionId);
            Thread.Sleep(3000);

            ProgressHub.UpdateProgressBar(64, data.ConnectionId);
            Thread.Sleep(2000);

            ProgressHub.UpdateProgressBar(77, data.ConnectionId);
            Thread.Sleep(2000);

            ProgressHub.UpdateProgressBar(92, data.ConnectionId);
            Thread.Sleep(3000);

            ProgressHub.UpdateProgressBar(99, data.ConnectionId);
            Thread.Sleep(2000);

            ProgressHub.UpdateProgressBar(100, data.ConnectionId);
        }
    }
}
اگر برنامه شما وب فرم است، باید تنظیمات مسیریابی ذیل را نیز به آن افزود. در برنامه‌های MVC4 این تنظیم به صورت پیش فرض وجود دارد:
using System;
using System.Web.Http;
using System.Web.Routing;

namespace WebFormsSample03
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // Register the default hubs route: ~/signalr
            RouteTable.Routes.MapHubs();

            RouteTable.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}
کاری که در این کنترلر انجام شده، شبیه سازی یک عملیات طولانی توسط متد Thread.Sleep است. همچنین این کنترلر، id کلاینت درخواست کننده یک url را نیز دریافت می‌کند. بنابراین می‌توان به نحو بهینه‌ای، تنها نتایج پیشرفت عملیات را به این کلاینت ارسال کرد و نه به سایر کلاینت‌ها.
همچنین در اینجا با توجه به مسیریابی تعریف شده، باید اطلاعات را به آدرس api/Downloader از نوع Post ارسال کرد.


تعریف کلاینت متصل به Hub

در سمت سرور، متد پویای updateProgressBar فراخوانی شده است. اکنون باید این متد را در سمت کلاینت پیاده سازی کنیم:
    <form id="form1" runat="server">
    <div>
    <input id="txtUrl" value="http://www.site.com/file.rar" type="text" />
        <input id="send" type="button" value="start download ..." />
        <br />
        <div id="bar" style="border: #000 1px solid; width:300px;"></div>
    </div>
    </form>
    <script type="text/javascript">
        $(function () {
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند
            var progressHub = $.connection.progressHub; //این نام مستعار پیشتر توسط ویژگی نام هاب تنظیم شده است
            progressHub.client.updateProgressBar = function (value) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#bar").html(GaugeBar.generate(value));
            };
            $.connection.hub.start() // فاز اولیه ارتباط را آغاز می‌کند
            .done(function () {
                $("#send").click(function () {
                    $("#send").attr('disabled', 'disabled');
                    var myClientId = $.connection.hub.id;
                    // اکنون اتصال برقرار است به سرور
                    $.ajax({
                        type: "POST",
                        contentType: "application/json",
                        url: "/api/Downloader",
                        data: JSON.stringify({ Url: $("#txtUrl").val(), ConnectionId: myClientId })
                    }).success(function () {
                        $("#send").removeAttr('disabled');
                    }).fail(function () {
                        //                    
                    });
                });
            });
        });
    </script>
بر روی این فرم، یک جعبه متنی که Url را دریافت می‌کند و یک دکمه‌ی آغاز کار دریافت این Url، وجود دارد.
در ابتدای کار صفحه، اتصال به progressHub برقرار می‌شود. اگر دقت کنید، نام این هاب با حروف کوچک در اینجا (در سمت کلاینت) آغاز می‌گردد.
سپس با تعریف یک callback به نام progressHub.client.updateProgressBar، پیام‌های دریافتی از طرف سرور را به یک افزونه progress bar جی‌کوئری، برای نمایش ارسال می‌کند.
کار اتصال به رویداد کلیک دکمه‌ی آغاز دریافت فایل، در متد done باید انجام شود. این callback زمانی فراخوانی می‌گردد که کار اتصال به سرور با موفقیت صورت گرفته باشد.
سپس در ادامه توسط jQuery Ajax، اطلاعات Url و همچنین Id کلاینت را به مسیر api/Downloader یا همان web api controller ارسال می‌کنیم.



کدهای کامل این مثال را از اینجا نیز می‌توانید دریافت نمائید:
  WebFormsSample03.zip
مطالب
MVVM و رویدادگردانی - قسمت دوم

قسمت اول این بحث و همچنین پیشنیاز آن‌را در اینجا و اینجا می‌توانید مطالعه نمائید.
همه‌ی این‌ها بسیار هم نیکو! اما ... آیا واقعا باید به ازای هر روال رویدادگردانی یک Attached property نوشت تا بتوان از آن در الگوی MVVM استفاده کرد؟ برای یکی دو مورد شاید اهمیتی نداشته باشد؛ اما کم کم با بزرگتر شدن برنامه نوشتن این Attached properties تبدیل به یک کار طاقت فرسا می‌شود و اشخاص را از الگوی MVVM فراری خواهد داد.
برای حل این مساله، تیم Expression Blend راه حلی را ارائه داده‌اند به نام Interaction.Triggers که در ادامه به توضیح آن پرداخته خواهد شد.
ابتدا نیاز خواهید داشت تا SDK‌ مرتبط با Expression Blend را دریافت کنید: (^)
سپس با فایل System.Windows.Interactivity.dll موجود در آن کار خواهیم داشت.

یک مثال عملی:
فرض کنید می‌خواهیم رویداد Loaded یک View را در ViewModel دریافت کنیم. زمان وهله سازی یک ViewModel با زمان وهله سازی View یکی است، اما بسته به تعداد عناصر رابط کاربری قرار گرفته در View ، زمان بارگذاری نهایی آن ممکن است متفاوت باشد به همین جهت رویداد Loaded برای آن درنظر گرفته شده است. خوب، ما الان در ViewModel نیاز داریم بدانیم که چه زمانی کار بارگذاری یک View به پایان رسیده.
یک راه حل آن‌را در قسمت قبل مشاهده کردید؛ باید برای این کار یک Attached property جدید نوشت چون نمی‌توان Command ایی را به رویداد Loaded انتساب داد یا Bind کرد. اما به کمک امکانات تعریف شده در System.Windows.Interactivity.dll به سادگی می‌توان این رویداد را به یک Command استاندارد ترجمه کرد:

<Window x:Class="WpfEventTriggerSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:vm="clr-namespace:WpfEventTriggerSample.ViewModels"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<vm:MainWindowViewModel x:Key="vmMainWindowViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainWindowViewModel}}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Loaded">
<i:InvokeCommandAction Command="{Binding DoLoadCommand}"
CommandParameter="I am loaded!" />
</i:EventTrigger>
</i:Interaction.Triggers>

<TextBlock Text="Testing InvokeCommandAction..."
Margin="5" VerticalAlignment="Top" />
</Grid>
</Window>

ابتدا ارجاعی به اسمبلی System.Windows.Interactivity.dll باید به پروژه اضافه شود. سپس فضای نام xmlns:i باید به فایل XAML جاری مطابق کدهای فوق اضافه گردد. در نهایت به کمک Interaction.Triggers آن، ابتدا نام رویداد مورد نظر را مشخص می‌کنیم (EventName) و سپس به کمک InvokeCommandAction، این رویداد به یک Command استاندارد ترجمه می‌شود.
ViewModel این View هم می‌تواند به شکل زیر باشد که با کلاس DelegateCommand آن در پیشنیازهای بحث جاری آشنا شده‌اید.

using WpfEventTriggerSample.Helper;

namespace WpfEventTriggerSample.ViewModels
{
public class MainWindowViewModel
{
public DelegateCommand<string> DoLoadCommand { set; get; }
public MainWindowViewModel()
{
DoLoadCommand = new DelegateCommand<string>(doLoadCommand, canDoLoadCommand);
}

private void doLoadCommand(string param)
{
//do something
}

private bool canDoLoadCommand(string param)
{
return true;
}
}
}

به این ترتیب حجم قابل ملاحظه‌ای از کد نویسی Attached properties مورد نیاز، به ساده‌ترین شکل ممکن، کاهش خواهد یافت.
بدیهی است این Interaction.Triggers را جهت تمام عناصر UI ایی که حداقل یک رویداد منتسب تعریف شده داشته باشند، می‌توان بکار گرفت؛ مثلا تبدیل رویداد Click یک دکمه به یک Command استاندارد:

<Button>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding DoClick}"
CommandParameter="I am loaded!" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>