مطالب
نصب و راه اندازی SQL Server بر روی لینوکس با استفاده از Docker
 چند وقتی است مایکروسافت تعدادی از محصولات خود را به صورت سورس باز در اختیار برنامه نویسان قرار داده‌است. برای مثال شما می‌توانید در لینوکس یا مک، از نسخه‌ی net core. آن استفاده کنید. در این مقاله روش اجرای sql server را در لینوکس، با استفاده از docker؛ تشریح خواهیم کرد. همچنین با یک پروژه ساده net core.، بر روی دیتابیس add-migration را اجرا کرده و همچنین چند رکورد را در جدولی ثبت می‌کنیم. البته می‌توان نسخه‌ی نصبی sql server را بدون نیاز به docker نیز دانلود و آن را بر روی لینوکس نصب کرد.  


در این مقاله چه چیزی را پوشش خواهیم داد:‌

· راه اندازی داکر
· پیکره‌بندی container image
· وصل شدن به sql
· ساخت یک پروژه ساده net core.
· ایجاد دیتابیس
· ثبت رکورد در دیتابیس

 قبل از هرچیز باید داکر را بر روی سیستم عامل خود (لینوکس) نصب نماید. چون نصب داکر بر روی لینوکس از حوصله‌ی این مقاله خارج می‌باشد، می‌توانید با مراجعه به این لینک docker را نصب کنید. پس از نصب docker، برای اطمینان حاصل نمودن از نصب، با دستور docker version می‌توان کانفیگ داکر را مشاهده کرد:
 


بعد از اینکه docker را بر روی سیستم خود نصب کردید، می‌توانید از دستورات داکر استفاده کنید. در این مقاله می‌خواهیم sql server را بر روی داکر نصب و راه اندازی کنیم.
 

دانلود و نصب sql server بر روی داکر

ابتدا وارد این لینک شوید. همانطور که مشاهده میکنید، SQL Server در 3 نسخه‌ی ویندوز، لینوکس و docker قابل دانلود می‌باشد. چون میخواهیم sql server را بر روی docker نصب کنیم، پس گزینه‌ی docker را انتخاب کنید.


قبل از هرچیز باید Image اس‌‌کیوال سرور را بر روی داکر دانلود نمائید. برای این کار وارد سایت dockerhub شوید و عبارت microsoft/mssql-server-linux را جستجو کنید.


همانطور که در تصویر نیز مشاهد می‌کنید، این بسته 10 میلیون بار دریافت شده‌است! در ادامه دستور زیر را در ترمینال خود Paste کنید و منتظر بمانید تا دانلود شود:
docker pull microsoft/mssql-server-linux:2017-latest
همچنین با اسکرول کردن در این صفحه می‌توانید آموزش نصب و راه ندازی این image را ببینید. بعد از دانلود image مخصوص داکر، با دستور docker images all می‌توانید images دانلود شده را مشاهده کنید. ولی image‌ها به خودی خود  کاربردی ندارند و باید آن‌ها را اجرا کنیم.
برای اجرای image sql از دستور زیر استفاده میکنیم:
 sudo docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=<YourStrong!Passw0rd>' \
-p 1433:1433 --name sql1 \
-d mcr.microsoft.com/mssql/server:2017-latest
در این دستور:
docker run –name sql : کار ساخت و اجرای Docker container ای به نام sql را انجام می‌دهد.
'e 'ACCEPT_EULA=Y- : سبب قرار دادن مقدار yes در ACCEPT_EULA که در قسمت environment variables تعریف شده‌است، می‌شود.
Set the  SA_PASSWORD : پسورد  environment variable ای که شما انتخاب می‌کنید.
p 1433:1433- : شماره پورتی که Docker container بر روی آن اجرا میشود.
-d microsoft/mssql-server-linux:2017-latest : نام Image ای که می‌خواهیم اجرا کنیم.

همانطور که مشاهده می‌کنید، Docker container بر روی پورت 1433 اجرا می‌شود. برای مشاهده جزئیات بیشتر، با وارد کردن دستو docker ps a می‌توان لیست containerها و وضعیت آن‌ها را مشاهده کرد.


همانطور که ملاحظه میکنید، در قسمت status، عبارت up به معنای در حال اجرا بودن container است. اگر عبارت دیگری را مشاهده کردید، با دستور dockr start id و وارد کردن شماره image خود می‌توانید آن را اجرا کنید.

تا اینجا توانستیم sql server  را اجرا کنیم. برای توضیحات بیشتر به این لینک مراجعه کنید.
 

وصل شدن به sql
برای وصل شدن به دیتابیس باید connection string دیتابیس مربوطه را داشته باشیم. با توجه به کانفیگ‌هایی که در بالا انجام دادیم، connection string ما به شکل زیر خواهد بود:
Server Host: localhost
Port: 1433
Authentication: SQL Server Authentication
Login: SA
Password: <StrongPasswordYouSet>
اگر کانکشن را به درستی کانفیگ کرده باشید، باید یک دیتابیس به نام انتخابی شما ایجاد شده باشد. در ادامه همین کار را بر روی یک پروژه‌ی  ساده netcore. انجام خواهیم داد. اما برای وصل شدن از طریق docker باید ابتدا bash (دستورات sqlcmd) را بارگذاری کنیم، تا بتوانیم به sqlcmd بر روی container در حال اجرا، دسترسی پیدا کنیم:
sudo docker exec -it sql1 "bash"
پس از آن باید sqlcmd  را به صورت مستقیم و از آدرس فیزیکی سیستم، درون container بارگذاری کنید:
/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '<YourNewStrong!Passw0rd>'
اگر دستور فوق با موفقیت اجرا شود ، عبارت 1> در ترمینال به نمایش در می‌آید. یعنی هم اکنون می‌توانید با تایپ دستوارت، آن‌ها را در sqlcmd اجرا کنید:


تا اینجای کار sql server  آماده‌ی اجرا دستورات شما می‌باشد. در ادامه می‌خواهیم چند دستور ساده‌ی sql را بر روی آن اجرا کنیم.


ساخت دیتابیس
با دستور sqlcmd زیر، ابتدا یک دیتابیس را میسازیم:
 CREATE DATABASE TestDB

ساخت جدول
در ادامه، دستور زیر را برای ساخت جدول مینویسیم:
 CREATE TABLE Inventory (id INT, name NVARCHAR(50), quantity INT)

ایجاد رکورد
مرحله بعدی، ایجاد یک رکورد جدید در دیتابیس میباشد:
 INSERT INTO Inventory VALUES (1, 'banana', 150); INSERT INTO Inventory VALUES (2, 'orange', 154);

در آخر با استفاده از دستور go، کوئری‌های بالا را اجرا می‌کنیم. اکنون باید یک دیتابیس جدید به نام TestDB و یک جدول جدید نیز به نام Inventory همچنین یک رکورد جدید در آن ثبت شده باشد. برای مشاهده‌ی تغییرات بالا، از دستورات زیر استفاده میکنیم:
- با دستور زیر لیست دیتابیس‌های موجود را می‌توان دید:
 SELECT Name from sys.Databases
- کو ئری select از دیتابیس:
 SELECT * FROM Inventory WHERE quantity > 152;

و با استفاده از دستور quit میتوانید از cmd خارج شوید.

تا اینجا توانستیم docker را بر روی سیستم راه ندازی و همچنین sql server  را بر روی آن نصب و اجرا کنیم. همچنین با دستورات sqlcmd توانستیم بر روی sql کوئری بزنیم.


ساخت و وصل شدن یک پروژه‌ی net core. و وصل شدن به sql server

حال میخواهیم با یک پروژه‌ی ساده‌ی net core. به sql server فوق وصل شده و یک جدول را به دیتابیس مذکور اضافه کرده و یک کوئری اضافه کردن رکوردی را به آن جدول بنویسیم. برای شروع، یک پروژه‌ی خالی net core. را ایجاد می‌کنیم. برای مثال یک پروژه‌ی api را ایجاد میکنیم:
dotnet new webapi -o dockerapi
سپس دو پکیج زیر را به آن اضافه میکنیم:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
در این مثال می‌خواهیم جدول Students را ایجاد و یک رکورد را در آن ثبت نماییم. پس یک کلاس را به نام Students ساخته و property‌های زیر را در آن مینویسیم:
public class Students
{
       public int Id { get; set; }
       public string Name { get; set; }
       public string Phone { get; set; }
}
مرحله‌ی بعد، ساخت context میباشد. برای اینکه وارد جزئیات نشویم، از قابلیت Scaffold استفاده می‌کنیم و context را تولید میکنیم:
 dotnet ef dbcontext scaffold "Server=localhost,1433\\Catalog=tutorial_database;Database=<YOUR_DATABASE_NAME>;User=SA;Password=<StrongPasswordYouSet>;" Microsoft.EntityFrameworkCore.SqlServer
پس از اجرای دستور بالا، context ساخته میشود. حال دورن context، یک DbSet را از students ایجاد میکنیم. بعد نوبت به تنظیم کردن connection string می‌رسد. داخل کانتکست، connection string را تنظیم کنید. همچنین connection string داخل appsettings.json  را نیز تنظیم کنید:
"ConnectionStrings": {
  "TestingDatabase": "Server=localhost:1433\\Database=<YourDatabaseName>;User=SA;Password=<StrongPasswordYouSet>;"
}
بعد از تنظیم کردن connection string، باید migration را بزنیم تا تغییرات context را مشاهده کنیم. با دستور زیر migration خود را اضافه کنید:
 dotnet ef migrations add <NAME_OF_MIGRATION>


همانطور که مشاهده می‌کنید، migrations اضافه شده و موجودیت هم اضافه شده‌است. حال باید بر روی migrations خود آپدیت بزنیم:
ef database update

همانطور که در شکل بالا نیز مشاهده می‌کنید، دیتابیس ما ایجاد شده‌است. حال به docker برمی‌گردیم و با دستور زیر، لیست تمام دیتابیس‌های موجود را نمایش میدهیم:

همانطور که مشاهده می‌کنید، دیتابیس برای ما ایجاد شده. با دستور زیر می‌توان جدول دیتابیس را مشاهده کرد:
 SELECT TABLE_NAME FROM dockerdb.INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'
مطالب
ASP.NET Web API - قسمت اول
بخش هایی از کتاب "مرجع کامل ASP.NET MVC (با پوشش کامل ASP.NET MVC 4)"
ترجمه و تالیف: بهروز راد
وضعیت: در دست چاپ


Web API چیست؟

Web API، نوع قالب جدیدی برای پروژه‌های مبتنی بر وب در NET. است که بر مبنای اصول و الگوهای موجود در ASP.NET MVC ساخته شده است و همراه با ASP.NET MVC 4 وجود دارد. Web API توسعه گران را قادر می‌سازد تا با استفاده از یک الگوی ساده که در Controllerها پیاده سازی می‌شود، وب سرویس‌های مبتنی بر پروتوکل HTTP را با کدها و تنظیمات کم ایجاد کنند. این سبک جدید برای ایجاد وب سرویس ها، می‌تواند در انواع پروژه‌های NET. مانند ASP.NET MVC، ASP.NET Web Forms، Windows Application و ... استفاده شود.
یک سوال کاملاً منطقی در اینجا به وجود می‌آید. چرا نیاز به بستری جدید برای ایجاد وب سرویس داریم؟ آیا در حال حاضر مایکروسافت بستری محبوب و فراگیر برای توسعه‌ی وب سرویس هایی که بتوانند با پروتوکل SOAP تعامل داشته باشند در اختیار ندارد؟ مگر وب سرویس‌های ASMX از زمان معرفی ASP.NET وجود نداشته اند؟ آیا تکنولوژی WCF مایکروسافت، بیشترین انعطاف پذیری و قدرت را برای تولید وب سرویس‌ها در اختیار قرار نمی‌دهد؟ وب سرویس‌ها جایگاه خود را یافته اند و توسعه گران با تکنولوژی‌های موجود به خوبی آنها را پیاده سازی و درک می‌کنند. چرا Web API؟

چرا Web Api؟
برای پاسخ به این سوال، باید برخی مشکلات را بررسی کنیم و ببینیم ابزارهای موجود چه راه حلی برای آنها در نظر گرفته اند. اگر با گزینه هایی که در ادامه می‌آیند موافق هستید، خواندن این مطلب را ادامه دهید، و اگر اعتقادی به آنها ندارید، پس نیازهای شما به خوبی با بسترهای موجود پاسخ داده می‌شوند.
  • من معتقد هستم که راه بهتری برای ایجاد وب سرویس‌ها وجود دارد.
  • من معتقد هستم که روش‌های ساده‌تری برای ایجاد وب سرویس‌ها وجود دارد و WCF بیش از حد پیچیده است.
  • من معتقد هستم که تکنولوژی‌های پایه‌ی وب مانند اَفعال GET، POST، PUT و DELETE برای انجام اَعمال مختلف توسط وب سرویس‌ها کافی هستند.
اگر همچنان در حال خواندن این مطلب هستید، توضیحات خود را با شرح تفاوت میان Web API و تکنولوژی‌های دیگر هم حوزه‌ی آن ادامه می‌دهیم و خواهید دید که استفاده از Web API چقدر آسان است.

تفاوت Web API و WCF
وب سرویس‌های ASMX تا چندین سال، انتخاب اول برای ایجاد وب سرویس‌های مبتنی بر پروتوکل SOAP با استفاده از پروتوکل HTTP بودند. وب سرویس‌های ASMX، از وب سرویس‌های ساده که نیاز به قابلیت تعامل پایین داشتند و در نتیجه به پروتوکل SOAP نیز وابسته نبودند پشتیبانی نمی‌کردند. WCF جای وب سرویس‌های ASMX را گرفت و خود را به عنوان آخرین و بهترین روش برای ایجاد وب سرویس‌ها در بستر NET. معرفی کرد. نمونه ای از یک سرویس WCF بر مبنای پروتوکل HTTP در NET. به صورت ذیل است.
[ServiceContract]
public interface IService1                       
{
    [OperationContract]                                 
    string GetData(int value);
    [OperationContract]
    CompositeType GetDataUsingDataContract(CompositeType composite);
}
...
public class Service1 : IService1                         
{
    public string GetData(int value)
    {
        return string.Format("You entered: {0}", value);
    }

    public CompositeType GetDataUsingDataContract(CompositeType composite)
    {
        if (composite == null)
        {
            throw new ArgumentNullException("composite");
        }
        if (composite.BoolValue)
        {
            composite.StringValue += "Suffix";
        }
        return composite;
    }
}
در WCF، پایه و اساس وب سرویس را یک interface تشکیل می‌دهد. در حقیقت اجزای وب سرویس را باید در یک interface تعریف کرد. هر یک از متدهای وب سرویس در interface تعریف شده که صفت OperationContract برای آنها در نظر گرفته شده باشد، به عنوان یکی از اَعمال و متدهای قابل فراخوانی توسط استفاده کننده از وب سرویس در دسترس هستند. سپس کلاسی باید ایجاد کرد که interface ایجاد شده را پیاده سازی می‌کند. در قسمت بعد، با مفاهیم پایه‌ی Web API و برخی کاربردهای آن در محیط ASP.NET MVC آشنا می‌شوید.

نتیجه گیری
Web API، یک روش جدید و آسان برای ایجاد وب سرویس ها، بر مبنای مفاهیم آشنای ASP.NET MVC و پایه‌ی وب است. از این روش می‌توان در انواع پروژه‌های NET. استفاده کرد.
مطالب
راه اندازی 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 وارد کنیم:


نظرات مطالب
C# 8.0 - Nullable Reference Types
بهبودهای نوع‌های ارجاعی نال‌نپذیر در NET Core 3.0 Preview 7.

پس از نصب SDK جدید، نحوه‌ی فعالسازی این قابلیت در فایل csproj، به صورت زیر درآمده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

این موارد در Preview 7 جدید هستند:
1) امکان اضافه کردن قید جدید notnull در حین تعریف نوع‌های جنریک
    interface IDoStuff<TIn, TOut> where TIn : notnull where TOut : notnull
    {
        TOut DoStuff(TIn input);
    }

2) اضافه شدن یک سری ویژگی توکار جدید برای «پیش» بررسی کار با نوع‌های ارجاعی نال نپذیر

ویژگی AllowNull به فراخوان‌ها امکان ارسال نال را حتی اگر مجاز نباشد، می‌دهد.  این ویژگی جدید در فضای نام System.Diagnostics.CodeAnalysis تعریف شده‌است. برعکس آن ویژگی DisallowNull، سبب خواهد شد تا فراخوان‌ها حتی در صورت مجاز بودن نیز نتوانند نال ارسال کنند.
using System;
using System.Diagnostics.CodeAnalysis;

namespace ConsoleApp
{
    public class MyClass
    {
        private string _innerValue = string.Empty;

        [AllowNull]
        public string MyValue
        {
            get
            {
                return _innerValue;
            }
            set
            {
                _innerValue = value ?? string.Empty;
            }
        }
    }
در مثال فوق ویژگی AllowNull سبب می‌شود تا در قسمت setter امکان دریافت نال نیز میسر شود؛ برای مثال جهت سازگاری با نگارش‌های قبلی برنامه.
دو ویژگی یاد شده را می‌توان بر روی پارامترهای متدها، پارامترهایی از نوع in و ref، فیلدها، خواص و ایندکسرها اعمال کرد.

3) اضافه شدن یک سری ویژگی توکار جدید برای «پس» بررسی کار با نوع‌های ارجاعی نال نپذیر

دو ویژگی جدید MaybeNull و NotNull کار پس بررسی نال پذیری را انجام می‌دهند:
    public class MyArray
    {
        // Result is the default of T if no match is found
        [return: MaybeNull]
        public static T Find<T>(T[] array, Func<T, bool> match)
        {
            //...
        }

        // Never gives back a null when called
        public static void Resize<T>([NotNull] ref T[]? array, int newSize)
        {
            //...
        }
    }
در این مثال، متد Find با تعریف ویژگی return: MaybeNull، ممکن است نال برگرداند. برای مثال اگر چیزی یافت نشد، default را بر گرداند.
در متد Resize، پارامتر array، می‌تواند نال دریافت کند، چون نال‌پذیر تعریف شده‌است؛ اما چون به ویژگی NotNull مزین است، حاصل تغییرات بر روی آن (خروجی از متد، از طریق پارامتری از نوع ref) نمی‌تواند نال باشد.

دو ویژگی یاد شده را می‌توان بر روی خروجی متدها، پارامترهایی از نوع out و ref، فیلدها، خواص و ایندکسرها اعمال کرد.

4) اضافه شدن یک سری ویژگی توکار جدید برای «پس» بررسی «شرطی» کار با نوع‌های ارجاعی نال نپذیر

در مثال‌های زیر کاربردهای دو ویژگی شرطی جدید NotNullWhen و MaybeNullWhen را مشاهده می‌کنید:
    public class MyString
    {
        // True when 'value' is null
        public static bool IsNullOrEmpty([NotNullWhen(false)] string? value)
        {
            //...
        }
    }
در اینجا با بکارگیری ویژگی [NotNullWhen(false)] به فراخوان اعلام می‌کنیم که اگر IsNullOrEmpty مقدار false را بر‌گرداند، مقدار value ارسال شده‌ی به آن، نال نیست.

    public class MyVersion
    {
        // If it parses successfully, the Version will not be null.
        public static bool TryParse(string? input, [NotNullWhen(true)] out Version? version)
        {
            //...
        }
    }
در اینجا با بکارگیری ویژگی [NotNullWhen(true)] به فراخوان اعلام می‌کنیم که اگر TryParse مقدار true را بر‌گرداند، مقدار version خروجی آن، نال نیست.

    public class MyQueue<T>
    {
        // 'result' could be null if we couldn't Dequeue it.
        public bool TryDequeue([MaybeNullWhen(false)] out T result)
        {
            //...
        }
    }
در اینجا با بکارگیری ویژگی [MaybeNullWhen(false)] به فراخوان اعلام می‌کنیم که اگر TryDequeue مقدار false را برگرداند، مقدار result خروجی آن، می‌تواند نال هم باشد.

5) اضافه شدن یک سری ویژگی توکار جدید برای شرط گذاشتن بین ورودی و خروجی، در حین کار با نوع‌های ارجاعی نال نپذیر

در متد زیر، هم خروجی و هم ورودی آن می‌توانند نال باشند. اما می‌خواهیم اگر path نال نباشد، اطمینان حاصل کنیم که استفاده کننده می‌داند، خروجی این متد، حتما نال نخواهد بود:
    class MyPath
    {
        [return: NotNullIfNotNull("path")]
        public static string? GetFileName(string? path)
        {
            //...
        }
    }
برای انجام یک چنین اطلاع رسانی‌هایی می‌توان از ویژگی جدید NotNullIfNotNull استفاده کرد. از آن می‌توان برای مزین سازی خروجی متدها و یا پارامترهایی از نوع ref استفاده کرد.

6) اضافه شدن یک سری ویژگی توکار جدید برای بررسی سیلان برنامه، در حین کار با نوع‌های ارجاعی نال نپذیر

در اینجا نحوه‌ی استفاده از دو ویژگی جدید DoesNotReturn و DoesNotReturnIf را مشاهده می‌کنید:
    internal static class ThrowHelper
    {
        [DoesNotReturn]
        public static void ThrowArgumentNullException(ExceptionArgument arg)
        {
            //...
        }
    }
اگر متد ThrowArgumentNullException در جائی فراخوانی شود، سبب بروز یک استثناء می‌شود. استفاده از DoesNotReturn سبب می‌شود تا به کامپایلر اعلام کند، پس از این نقطه، دیگر نیازی به بررسی نال بودن اشیاء نیست؛ چون آن قطعه از کد، غیرقابل اجرا و رسیدن می‌شود. این ویژگی را تنها بر روی متدها می‌توان قرار داد.

    public static class MyAssertionLibrary
    {
        public static void MyAssert([DoesNotReturnIf(false)] bool condition)
        {
            //...
        }
    }
اگر متد MyAssert فراخوانی شود و ورودی آن false باشد، یک استثناء را صادر می‌کند. با بکارگیری ویژگی [DoesNotReturnIf(false)] این موضوع را به کامپایلر اعلام کرده و از آن درخواست می‌کنیم تا کار بررسی نال بودن اشیاء را از آن سطر به بعد، انجام ندهد. این ویژگی را تنها بر روی پارامترها می‌توان اعمال کرد.
مطالب
الگوریتم‌های داده کاوی در SQL Server Data Tools یا SSDT - قسمت چهارم - الگوریتم‌ Clustering یا خوشه بندی
در قسمت قبل با الگوریتم های Decision trees و Linear Regression آشنا شدیم. در این قسمت به الگوریتم Clustering یا خوشه بندی می‌پردازیم.

مقدمه


تصور کنید شما بچه‌ای هستید که با یک کیسه تیله روی زمین نشسته‌اید. لحظه‌ای که تیله‌ها را از کیسه روی زمین می‌ریزید، متوجه می‌شوید که تیله‌ها، چهار رنگ دارند (آبی، قرمز، سبز و زرد). تیله‌ها را در چهار گروه با توجه به رنگ‌هایشان قرار می‌دهید. اما بعد متوجه می‌شوید که اندازه بعضی از تیله‌ها متوسط و برخی بزرگ و تعدادی هم کوچک هستند. شما تصمیم می‌گیرید که تیله‌های کوچک و متوسط، در کنار یکدیگر و در یک گروه قرار گیرند؛ اما تیله‌های بزرگ را در یک گروه دیگر قرار می‌دهید. چرا که تنها یکی از آن‌ها را باید به هر بازیکن داد. تبریک می‌گویم! شما یک عمل خوشه بندی را انجام دادید.
حال زمانیکه قدری با دقت بیشتری به خوشه بندی خود نگاه می‌کنید، متوجه می‌شوید که برخی از تیله‌ها کریستالی، برخی دیگر سه پر و چهارپر، بعضی از آن‌ها صاف و صیقلی و بعضی دیگر دارای خراش می‌باشند. اینجاست که قدری سردرگم می‌شوید. آیا باید همان گروه بندی ساده براساس رنگ و اندازه را مدنظر قرار دهید، یا بهتر است عوامل دیگری مانند سبک، مواد تشکیل دهنده و وضعیت ظاهری را نیز اضافه کنید؟
خوشه بندی، یک عمل انسانی راحت، طبیعی و حتی می‌شود گفت اتوماتیک برای مواجه شدن با مجموعه ویژگی‌های کوچک می‌باشد. اما همینطور که ویژگی‌ها بیشتر می‌شوند، حل مساله برای انسان خیلی سخت و غیرممکن می‌شود. ذهن یک انسان معمولی، تقریبا قادر به درنظر گرفتن 5 یا 6 بُعد می‌باشد. این درحالی است که مجموعه داده‌های مدرن گاها دارای ده‌ها بعد (اگر نگوییم صدها) می‌باشند.
الگوریتم خوشه بندی مایکروسافت، گروه بندی‌هایی ذاتی را داخل مجموعه داده شما پیدا می‌کند که ممکن است به چشم نیایند. به عبارت دیگر، متغیرهای پنهانی را که به طور دقیق داده‌های شما را خوشه بندی می‌کنند، پیدا می‌نماید. برای مثال فرض کنید که شما جزیی از یک گروه بزرگ مسافران هستید که در بخش نوار نقاله حمل بار در فرودگاه منتظر برداشتن چمدان می‌باشید. متوجه می‌شوید که درصد قابل توجهی از مسافران شلوار کوتاه پوشیده و پوستشان در اثر آفتاب قدری تیره‌تر شده است؛ درحالیکه مابقی مسافران لباس گرم مانند ژاکت و کت به تن دارند. بنابراین به یک حقیقت پی می‌برید. یک گروه از نواحی گرمسیری آمده‌اند و دیگری از یک جای سرد و مرطوب. این همان متغیر پنهان است.

الگوریتم Clustering یا خوشه بندی مایکروسافت  

الگوریتم خوشه بندی مایکروسافت رفتارهای خاصی را در مواجه با نوع ویژگی‌ها از خود نشان می‌دهد. در ارتباط با ستون‌های ورودی (Input) و ورودی-خروجی (Predict) مانند آنچه قبلا گذشت عمل می‌کند. البته با یک تفاوت و آن اینکه ستون‌های ورودی-خروجی در حین پیش بینی قابل انتخاب هستند؛ حال آنکه ستون‌های ورودی اینطور نیستند. ستون‌هایی که فقط خروجی (Predict Only) هستند، در طی فاز خوشه بندی برای آموزش مدل به کار نمی‌روند.
همانطور که قبلا نیز اشاره شد، خوشه بندی، رایج‌ترین عملی است که با این الگوریتم انجام می‌دهند. بنابراین جهت کشف خوشه بندی‌ها در یک مجموعه داده می‌توان این الگوریتم را روی مجموعه داده اعمال کرده و خوشه بندی‌های کشف شده را برچسب زد. بعد از برچسب زدن می‌توان از آن، جهت گزارش گیری و تحلیل داده‌ها استفاده نمود. از آنجا که این الگوریتم سربار پردازشی و حافظه‌ای زیادی دارد، بنابراین در رابطه با مجموعه داده‌های بزرگ (رکوردهای میلیونی و پیچیده) بهتر است که فقط بخش کوچکی از داده را برای آموزش استفاده کرده (که البته کافی و وافی است) و از طریق آن‌ها ویژگی‌های خوشه بندی را کشف کرد.
توسط این الگوریتم می‌توان مدل را تجزیه-تحلیل نمود و نابهنجاری‌ها را نیز تشخیص داد.

محتوای مدل خوشه بندی

درک محتوای مدل خوشه بندی بسیار ساده است. شکل زیر دیاگرام خوشه بندی یا Cluster Diagram می‌باشد. همانطور که در شکل آمده است SSAS در نشان دادن نام هر گره به خوبی عمل نمی‌کند زیرا هر گره توسط Cluster و یک ایندکس نشان داده می‌شود و نام معناداری برای آن در نظر نمی‌گیرد. برای مثال خوشه مربوط به تیله‌های آبی بزرگ سه پر (برای مثال Cluster2، Cluster1 و ....).


بنابراین برای برچسب زدن مناسب برروی هر گره باید به شکل زیر عمل کرد:
  • مرور اجمالی مدل: توسط دو برگه اول یعنی Cluster Diagram و Cluster Profiles می‌توان توپولوژی مدل خوشه بندی را به دست آورد. در برگه Cluster Diagram هر خوشه یک گره را تشکیل می‌دهد که براساس شباهت به یکدیگر متصل شده‌اند. بدیهی است خوشه‌هایی که در ضعیف‌ترین ارتباط هم به یکدیگر متصل نیستند، هیچگونه شباهتی ندارند. براساس میزان شباهت، نوار اتصال بین گره‌ها، تیره‌تر یا روشن‌تر می‌گردد. همانطور که در شکل فوق مشخص است هرچه این نوار تیره‌تر باشد، بیانگر شباهت بیشتر بین دو خوشه است. Cluster Profiles یک ستون را برای هر خوشه و یک سطر را برای هر ویژگی درنظر می‌گیرد. درصورتیکه یک ویژگی برای شما جالب توجه باشد می‌توانید به صورت افقی توزیع آن را در خوشه‌های مختلف مشاهده کنید. هر زمانیکه آیتمی نظر شما را جلب کرد می‌توان به سلول‌های مجاور یا سلول‌های هم خوشه آن نگاه کرد و مفهوم آن خوشه را بیشتر درک نمود. با کلیک برروی هر یک از سلول‌ها می‌توان جزییات مربوط به آن سلول را مشاهده کرد. برای مثال می‌توان فهمید این خوشه براساس چه شروطی ایجاد شده‌است. شکل زیر نمایی از Cluster Profiles را نشان می‌دهد. همانطور که در قسمت قبل نیز بحث شد، نوارهای هیستوگرام مربوط به ویژگی‌های گسسته بوده و نوارهای الماسی نشان دهنده ویژگی‌های پیوسته می‌باشند.


  • انتخاب یک خوشه و تشخیص وجه تمایز آن: از برگه Cluster Diagram شروع می‌نماییم. یک راه این است که ببینیم کدام خوشه‌ها، قوی‌ترین ارتباط را دارند و یکی از آن‌ها را انتخاب نماییم. راه دیگر این است که خوشه‌ای را انتخاب کنیم که به نظر دور  از بقیه خوشه‌ها است. پس از انتخاب خوشه موردنظر به تب Cluster Characteristics می‌رویم. همانطور که در شکل زیر مشخص است این بخش مشخصات حالات مختلف یک خوشه را توسط نمودار احتمال با روند کاهشی  نشان می‌دهد. بنابراین می‌توان متوجه شد چه ویژگی هایی و با چه احتمالی سبب ایجاد یک خوشه شده‌اند.

    ممکن است تعدادی ویژگی با احتمال بالا در یک خوشه وجود داشته باشند اما سوال اینجاست که از کجا معلوم که تمام این ویژگی‌ها در خوشه‌های دیگر نیز این احتمال را نداشته باشند؟ برای اینکه متوجه شویم که بیشتر چه ویژگی سبب وجه تمایز این خوشه شده‌است باید به برگه Cluster Discrimination مراجعه کنیم که نمونه‌ای از آن در شکل زیر آمده است.

     در این بخش می‌توان خصوصیات خوشه مدنظر را با خوشه‌های دیگر یا با متمم خوشه (Complement) مقایسه کرد و توسط آن، ویژگی‌هایی را که سبب وجه تمایز این خوشه شده‌اند، مشاهد نمود. توجه به این نکته ضروری است که نوار نشان داده شده در رابطه با هر ویژگی تنها نشان دهنده میزان توجه به آن ویژگی در آن خوشه است و به این معنی نیست که خوشه‌های دیگر عاری از آن ویژگی هستند.

  • تشخیص چگونگی تمایز یک خوشه از خوشه‌های نزدیک به آن: حال می‌توان با اطلاعاتی که تا به حال کسب کرده‌ایم یک خوشه را به صورت دقیق برچسب بزنیم. اما ممکن است این خوشه خیلی شبیه به خوشه‌های دیگر باشد و بنابراین مجبور شویم که یک برچسب را بر روی دو خوشه بزنیم. پس توصیه می‌شود که خوشه انتخاب شده را با خوشه‌های همسایه مقایسه کنیم. برای این منظور به تب Cluster Diagram مراجعه نموده و نگاه می‌کنیم که کدام خوشه‌ها به خوشه مدنظر ما نزدیک هستند. اگر هیچ اتصال قوی بین دو خوشه نبود کار تمام است. اما اگر اینگونه نبود آنگاه باید مجددا به تب Cluster Characteristics مراجعه نموده و تک تک ویژگی‌های دو خوشه نزدیک به هم را مقایسه نماییم، تا فرق بین آن‌ها را در صورت وجود به دست آوریم.

خوشه بندی سخت و خوشه بندی نرم

مهمترین فرق بین الگوریتم‌های خوشه بندی، روشی است که الگوریتم‌ها در رابطه با انتساب حالت‌ها، به خوشه‌ها اتخاذ می‌کنند. الگوریتم خوشه بندی مایکروسافت، دو روش مختلف را برای اینکار دارند: K-means و Expectation Maximization. روش اول (شکل سمت چپ) براساس فاصله حالت‌ها نسبت به خوشه‌ها، آن‌ها را نسبت می‌دهد و در پایان مرکز خوشه طوری قرار خواهد گرفت که وسط حالت‌ها باشد. به این تکنیک، خوشه بندی سخت می‌گویند (شکل سمت چپ) زیرا همانطور که در شکل سمت چپ مشخص است هر شیء فقط و فقط در یک خوشه قرار می‌گیرد و هیچ یک از خوشه‌ها با یکدیگر هم پوشانی ندارند. روش دوم (شکل سمت راست) به جای استفاده محض از مقیاس فاصله، از یک مقیاس احتمالی استفاده می‌کند. این روش یک منحنی زنگوله شکل را که دارای میانگین و انحراف معیار است برای هر بُعد درنظر می‌گیرد. چنانچه نقطه‌ای داخل یک منحنی بیفتد با یک احتمال معینی به آن خوشه نسبت داده می‌شود. به دلیل اینکه منحنی‌ها می‌توانند هم پوشانی داشته باشند، بنابراین هر نقطه می‌تواند به چندین خوشه منتسب شود؛ البته با احتمالات مختلف. به این تکنیک، خوشه بندی نرم گفته می‌شود (شکل سمت راست). این تکنیک در شناسایی خوشه‌های پیوسته خیلی موثر است مانند وضعیت تراکم جمعیت مناطق. 


خوشه بندی با قابلیت مدرج کردن

یکی از مسایلی که در الگوریتم خوشه بندی وجود دارد این است که جهت به دست آوردن خوشه بندی مناسب، نیاز به تکرار آموزش برروی داده‌ها است. این تکرار در مجموعه داده‌های کوچک، مشکلی ایجاد نمی‌کند، اما در رابطه با مجموعه داده‌های بزرگ این امر امکان پذیر نیست. زیرا کل مجموعه داده داخل رم قرار می‌گیرد و مشکلات کارآیی را ایجاد می‌کند. الگوریتم خوشه بندی مایکروسافت یک چارچوب برای مدرج کردن خوشه بندی را در اختیار ما قرار می‌دهد که با استفاده از آن می‌توان بر این مشکل فایق آمد. این مهم توسط پارامتر Sample_Size مرتفع می‌شود که یکی از پارامترهای این الگوریتم می‌باشد. همانطور که در قسمت قبل نیز گفته شد دسترسی به پارامترهای هر الگوریتم به شکل زیر صورت می‌پذیرد:
مراجعه به برگه mining models ، کلیک بر روی الگوریتم، رفتن به پنجره properties  الگوریتم. حال می‌توان  به بخش Algorithm Parameters رفت و پارامترها را مقداردهی کرد. البته اگر از نظر حافظه رم مشکلی ندارید، می‌توانید مقدار این پارامتر را صفر درنظر بگیرید و با این کار تمام حافظه رم را به پردازش الگوریتم اختصاص بدهید، تا الگوریتم به هر میزانی که نیاز دارد، از حافظه رم استفاده نماید.
مطالب
آشنایی با CLR: قسمت بیست و یکم

آغاز فصل سوم:

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

مشکلی که در توزیع اسمبلی‌های عمومی وجود دارد این است که شما باید این اطمینان را کسب کنید که اسمبلی شما، همیشه همان اسمبلی خواهد بود و تغییری در آن رخ نخواهد داد. فرض کنید که شما از یک اسمبلی که توسط شرکت تلریک تهیه شده است استفاده کرده‌اید و برنامه‌ی شما به خوبی با آن کار می‌کند. ولی چه اتفاقی می‌افتد که اگر برنامه‌ی دیگری بعد از شما نصب شود و از همان اسمبلی، منتها از نسخه‌ی دیگر آن استفاده می‌کند؟ بله برنامه‌ی شما احتمال زیادی دارد که در این حالت به مشکل بر بخورد یا اینکه شخص دیگری یک اسمبلی دیگری همنام اسمبلی و هم نسخه‌ی اسمبلی شما تولید می‌کند. برای رفع این مشکلات مایکروسافت تمهیداتی را اندیشیده است که ما به آن می‌گوییم «اسمبلی با نام قوی Strong Name Assembly».

اسمبلی‌ها به دو دسته تقسیم می‌شوند: اسمبلی‌های با نام قوی و اسمبلی هایی با نام ضعیف ( این مورد در مستندات مایکروسافت نیست و توسط نویسنده‌ی کتاب، این اصطلاح ایجاد شده است).

در قسمت دوم گفتیم که اسمبلی‌ها از قسمت‌هایی چون جداول مانیفست، هدرها، متادیتاها و ... تشکیل می‌شوند. اسمبلی‌های نام قوی هم به همین شکل هستند. فقط توسط جفت کلید عمومی و خصوصی محافظت و امضا می‌شوند که برای ناشر، یک کلید منحصر به فرد را ایجاد می‌کنند و به ناشر این اطمینان را می‌دهند که اگر جفت کلیدی را که در دست شما است، به کسی ندهید، هیچ کس دیگری نمی‌تواند اسمبلی را با مشخصات اسمبلی شما امضاء کند.

حال یک اسمبلی نام قوی، دارای چهار خصوصیت است: نام اسمبلی بدون پسوند، نگارش، فرهنگ (Culture) و کلید عمومی.

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

"MyTypes, Version=1.0.8123.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
"MyTypes, Version=1.0.8123.0, Culture="en­US", PublicKeyToken=b77a5c561934e089" 
"MyTypes, Version=2.0.1234.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
"MyTypes, Version=1.0.8123.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"

استفاده از فضای نام

System.Reflection.AssemblyName

به شما اجازه‌ی ساخت و دریافت اطلاعاتی را از اسمبلی‌ها می‌دهد؛ هر نوع اطلاعاتی را که شامل 4 خصوصیت بالا می‌شود، به شما می‌دهد.

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

مکانیزم اعمال این امضاء بدین شکل است که بعد از ساخت فایل PE که شامل مانیفست است، کل محتویات آن ( به جز امضاهای شناسایی و هدر (Checksum) و اطلاعات مربوط به نام قوی) هش می‌شوند و سپس این مقدا هش شده توسط کلید خصوصی امضاء می‌شود و در همان PE، در بخش هش نشده‌ای به نام Reserved Section ذخیره می‌گردند و در نهایت CLR Header برای ارتباط با این بخش آپدیت می‌شود. 

تصویر زیر توضیح بند بالا را نشان می‌دهد:

 

مختصری در مورد GAC

اگر مقاله‌ی بالا را خوانده باشید، الان باید بدانید که GAC، وظیفه‌ی رجیستر و توزیع اسمبلی نام قوی را بر عهده دارد؛ ولی چرا باید اینکار را به GAC، واگذار کنیم؟ فرض کنید دو اسمبلی با نام‌های dotnettips.dll وجود دارند. اگر قرار باشد هر دو داخل یک دایرکتوری قرار بگیرند، مسلما اسمبلی دوم بر روی اسمبلی اولی overwrite خواهد شد. درنتیجه کاری که GAC انجام می‌دهد، جلوگیری از این اتفاق است و GAC مدیریت می‌کند که در داخل مسیر
%SystemRoot%\Microsoft.NET\Assembly
زیرشاخه هایی ایجاد شوند و اسمبلی‌ها در آن‌ها قرار بگیرند.

هر اسمبلی را که توزیع می‌کنید، حتما حداقل به یک اسمبلی نام قوی ارجاع خواهد داشت؛ دلیل این گفته هم وجود فضای نام system.object در اسمبلی mscorlib است. برای همین، موقعیکه شما با csc، کامپایل می‌کنید، از سوئیچ reference استفاده می‌شود تا ارجاعی را به نام اسمبلی مورد نظر داشته باشد. اگر نام اسمبلی به طور کامل به همراه مسیر ذکر شود که مستقیما از همانجا فراخوانی می‌شود؛ در غیر این صورت مسیرهای زیر مورد بررسی قرار می‌گیرند:
  1. مسیر پوشه‌ی کاری برنامه
  2. مسیر کامپایلر CSC
  3. هر مسیری که با سوئیچ lib به کامپایلر معرفی کرده باشید.
  4. هر مسیری که با متغیرهای Lib Environment مشخص شده باشند.
بنابراین اگر شما مثلا از یک اسمبلی مانند system.drawing استفاده کرده باشید، کمپایلر به طور خودکار دستور زیر را صادر می‌کند:
/reference:System.Drawing.dll
و همانطور که گفته شد، کامپایلر شروع به اسکن دایرکتوری‌ها می‌کند و در نهایت آن را در مسیر خود کامپایلر، یعنی مورد 2 پیدا می‌کند. برنامه کمپایل می‌شود، ولی این اسمبلی همان اسمبلی نیست که برنامه در زمان اجرا، مورد استفاده قرار می‌دهد؛ چون این اسمبلی یک اسمبلی نام قوی است و انتظار می‌رود تا در مسیر مورد تایید GAC یافت شود. به همین دلیل است که موقع نصب دات نت فریم ورک، از هر اسمبلی، دو نسخه بر روی سیستم کپی می‌شود. یکی در مسیر کامپایل و دیگری برای GAC.
در ضمن اسمبلی‌های موجود در مسیر کامپایلر به هیچ عنوان اهمیتی به نوع ماشین نمی‌دهند؛ چون متادیتاهای اسمبلی آن‌ها اهمیت دارند نه کد IL آن‌ها. کد IL فقط در زمان اجرا، برای ما اهمیت دارد و در زمان کامپایل، همان متادیتا کفایت می‌کند و در نهایت برنامه موقع اجرا، با توجه به نوع ماشین x86,x36,ARM، اسمبلی مورد نیاز خود را از طریق GAC فراهم می‌کند که هر یک از اسمبلی‌های مخصوص هر ماشین، توسط GAC، در داخل زیر شاخه‌های مخصوص خود قرار گرفته‌اند.
مطالب
مروری بر تاریخچه محدودیت حافظه مصرفی برنامه‌های ASP.NET در IIS

زمانیکه اولین نگارش ASP.NET‌ حدود 10 سال قبل منتشر شد،‌ تنها سیستم عاملی که از آن پشتیبانی می‌کرد، ویندوز سرور 2000 بود، تنها پروسه‌ی اجرایی آن aspnet_wp نام داشت و تنها معماری پشتیبانی شده هم X86 بود. به پروسه‌ی aspnet_wp محدودیت مصرف حافظه‌ای اعمال شده بود که در حین آغاز آن بر اساس مقدار قابل تغییر processModel memoryLimit محاسبه و اعمال می‌شد (تعریف شده در فایل ماشین کانفیگ). این عدد به صورت درصدی از ظرفیت RAM فیزیکی سیستم، قابل تعریف و به صورت پیش فرض به 60 درصد تنظیم شده بود. به این ترتیب این پروسه مجاز نبود تا تمام حافظه‌ی فیزیکی مهیا را مصرف کند و در صورت وجود نشتی حافظه‌ای در برنامه‌ای خاص، این پروسه امکان بازیابی مجدد حافظه را پیدا می‌کرد (recycling). همچنین یک مورد دیگر را هم باید در نظر داشت و آن هم وجود قابلیتی است به نام ASP.NET Cache است که امکان ذخیره سازی مقادیر اشیاء را در حافظه‌ی مصرفی این پروسه مهیا می‌سازد. هر زمان که میزان این حافظه‌ی مصرفی به حد نزدیکی از محدودیت تعریف شده برسد، این پروسه به صورت خودکار شروع به حذف آن‌ها خواهد کرد.
محدودیت 60 درصدی تعریف شده، برای سیستم‌هایی با میزان RAM کم بسیار مفید بود اما در سیستم‌هایی با میزان RAM بیشتر، مثلا 4 گیگ به 2.4GB حافظه مهیا (60 درصد حافظه فیزیکی سیستم) محدود می‌شد و همچنین باید در نظر داشت که میزان user mode virtual address space مهیا نیز تنها 2 گیگابایت بود. بنابراین هیچگاه استفاده مؤثری از تمام ظرفیت RAM مهیا صورت نمی‌گرفت و گاها مشاهده می‌شد که یک برنامه تنها با مصرف 1.5GB RAM می‌توانست پیغام OutOfMemoryException را صادر کند. در این حالت مطابق بررسی‌های صورت گرفته مشخص شد که اگر مقدار processModel memoryLimit به حدود 800 مگابایت تنظیم شود، بهترین عملکرد را برای سیستم‌های مختلف می‌توان مشاهده کرد.

با ارائه‌ی ویندوز سرور 2003 و همچنین ارائه‌ی نسخه‌ی 1.1 دات نت فریم ورک و ASP.NET ، این وضعیت تغییر کرد. پروسه‌ی جدید در اینجا w3wp نام دارد و این پروسه تعاریف مرتبط با محدودیت حافظه‌ی خود را از تنظیمات IIS دریافت می‌کند (قسمت Maximum Used Memory در برگه‌ی Recycling مربوط به خواص Application Pool مرتبط). متاسفانه این عدد به صورت پیش فرض محدودیتی ندارد و به ظاهر برنامه مجاز است تا حد امکان از حافظه‌ی مهیا استفاده کند. به همین جهت یکی از مواردی را که باید در نظر داشت، مقدار دهی Maximum Used Memory ذکر شده است. خصوصا اینکه در نگارش 1.1 ، تنظیمات میزان مصرف RAM مرتبط با ASP.NET Cache نیز با برنامه یکی است.

در نگارش 2.0 دات نت فریم ورک، تنظیمات مرتبط با ASP.NET cache از تنظیمات میزان RAM مصرفی یک برنامه‌ی ASP.NET جدا شد و این مورد توسط قسمت cache privateBytesLimit قابل تنظیم و مدیریت است (در فایل IIS Metabase و همچنین فایل web.config برنامه).

نکته!
اگر process memory limit و همچنین cache memory limit را تنظیم نکنید، باز به همان عدد 60 درصد سابق بازخواهیم گشت و این مورد به صورت خودکار توسط IIS محاسبه و اعمال می‌شود. البته محدودیت ذکر شده برای پروسه‌های 64 بیتی در این حالت بسیار بهتر خواهد بود. اگر هر دوی این‌ها را تنظیم کنید، عدد حداقل بکارگرفته شده، مبنای کار خواهد بود و اگر تنها یکی را تنظیم کنید ، این عدد به هر دو حالت اعمال می‌گردد. برای بررسی بهتر می‌توان به مقدار Cache.EffectivePrivateBytesLimit و Cache.EffectivePercentagePhysicalMemoryLimit مراجعه کرد.

و ... اکنون بهتر می‌توانید به این سؤال پاسخ دهید که «سرور ما بیشتر از 4 گیگ رم دارد و برنامه‌ی ASP.NET من الان فقط 850 مگ رم مصرف کرده (که البته این هم نشانی از عدم dispose صحیح منابع است یا عدم تعیین تقدم و تاخر و زمان منقضی شدن، حین تعریف اشیاء کش)، اما پیغام out of memory exception را دریافت می‌کنم. چرا؟!»


بنابراین ایجاد یک Application pool جدید به ازای هر برنامه‌ی ASP.NET امری است بسیار مهم زیرا:
- به این ترتیب هر برنامه‌ی ASP.NET در پروسه‌ای ایزوله از پروسه‌ی دیگر اجرا خواهد شد (این مساله از لحاظ امنیتی هم بسیار مهم است). در اینجا هر برنامه، از پروسه‌ی w3wp.exe مجزای خاص خود استفاده خواهد کرد (شبیه به مرورگرهایی که هر tab را در یک پروسه جدید اجرا می‌کنند).
- اگر پروسه‌ای به حد بالای مصرف حافظه‌ی خود رسید با تنظیمات انجام شده در قسمت recycling مرتبط با Application pool اختصاصی آن، به صورت خودکار کار بازیابی حافظه صورت می‌گیرد و این امر بر روی سایر برنامه‌ها تاثیر نخواهد داشت (کاربران سایر برنامه‌ها مدام شکایت نمی‌کنند که سشن‌ها پرید. کش خالی شد. زیرا در حالت وجود application pool اختصاصی به ازای هر برنامه، مدیریت حافظه برنامه‌ها از هم ایزوله خواهند بود)
- کرش صورت گرفته در یک برنامه به دلیل عدم مدیریت خطاها، بر روی سایر برنامه‌ها تاثیر منفی نخواهد گذاشت. (زمانیکه ASP.NET worker process به دلیل استثنایی مدیریت نشده خاتمه یابد بلافاصله و به صورت خودکار مجددا «وهله‌ی دیگری» از آن شروع به کار خواهد کرد؛ یعنی تمام سشن‌های قبلی از بین خواهند رفت؛ که در صورت ایزوله سازی ذکر شده، سایر برنامه‌ها در امان خواهند ماند؛ چون در پروسه ایزوله‌ی خود مشغول به کار هستند)
- با وجود application pool اختصاصی به ازای هر برنامه، می‌توان برای سایت‌های کم ترافیک و پرترافیک، زمان‌های recycling متفاوتی را اعمال کرد. به این ترتیب مدیریت حافظه‌ی بهتری قابل پیاده سازی می‌باشد. همچنین در این حالت می‌توان مشخص کرد کدام سایت از تعداد worker process بیشتر یا کمتری استفاده کند.
- کاربری که پروسه‌ی ASP.NET تحت آن اجرا می‌شود نیز همینجا تعریف می‌گردد. بنابراین به این ترتیب می‌توان به برنامه‌ای دسترسی بیشتر و یا کمتر داد، بدون تاثیر گذاری بر روی سایر برنامه‌های موجود.

نتیجه گیری:
- از IIS استفاده می‌کنید؟ آیا می‌دانید Application pool چیست؟
- آیا می‌دانید در صورت عدم مقدار دهی پارامترهای حافظه‌ی یک Application pool ، به صورت پیش فرض چند درصد از حافظه‌ی فیزیکی مهیا در اختیار شما است؟


برای مطالعه بیشتر:

مطالب
سیستم‌های توزیع شده در NET. - بخش ششم- Message Broker

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

فرض کنید ما چندین سیستم بزرگ مرتبط یا غیر مرتبط به هم داریم که هر کدامشان از چندین زیر سیستم تشکیل شده‌اند. ارتباط و وابستگی این سیستم‌ها به این صورت است که یا از سیستم‌های دیگر سرویس‌هایی را دریافت می‌کنند، یا داده‌های مورد استفاده در آنها توسط سیستم‌های دیگر تولید می‌شوند و آنها در بازه زمانی مشخصی این داده‌ها را در سیستم‌های خودشان بروزرسانی می‌کنند. 

همانطور که می‌بینید کل سیستم ما از همکاری تعدادی سیستم بزرگ تشکیل شده‌است که هرکدام از این سیستمهای بزرگ نیز از همکاری دو نوع زیر سیستم تشکیل شده‌اند. نوع اول این زیرسیستمها که با عبارت Sub System مشخص شده‌اند، زیر سیستمهایی هستند که تنها در همان سیستم استفاده می‌شوند و نوع دوم که با عبارت Common Sub System مشخص شده‌اند و ما نیز زیاد با آنها روبرو می‌شویم، زیرسیستمهایی هستند که بصورت مشترک بین دو یا چند سیستم بزرگ مورد استفاده قرار می‌گیرند. بعنوان مثال می‌توان زیرسیستم ارسال پیامک، ارسال ایمیل، مدیریت Log، زیر سیستم پرداخت و هر نوع زیرسیستم دیگری را که در سیستمهای مختلف بصورت مشترک استفاده می‌شود، نام برد. 

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

بطور مثال زمانیکه اطلاعات پایه سیستم شما توسط یک یا چند سیستم تولید می‌شوند و سایر سیستم‌ها بخاطر پایین نیامدن کارآیی سیستم خودشان مجبورند داده‌های سیستم پایه را در سیستم خودشان Replicate کنند، شما چه روشی را برای بروز رسانی داده‌های سایر سیستم‌ها ارائه می‌دهید؟ شاید ساده‌ترین و اولین روشی که به ذهنمان برسد، ایجاد یک Job باشد که با اجرا شدن در بازه زمانی مشخصی (مثلا شبانه) عملیات بروزرسانی را انجام دهد. این روش دو مشکل اساسی دارد: اول اینکه بصورت RealTime نیست. یعنی از زمانیکه داده وارد سیستم پایه می‌شود، تا زمانیکه Job اجرا شود، سیستمهای دیگر نمی‌توانند از داده جدید استفاده کنند و دوم اینکه با اضافه شدن به تعداد سیستم‌های وابسته شما مجبورید یا یک Job جدید را برای آن پیاده سازی کنید، یا قبلی را تغییر دهید. شاید شما با پیاده سازی چند سرویس در سیستم‌های پایه و وابسته بتوانید مشکل RealTime نبودن بروز رسانی داده را حل کنید، اما این روش نیز بدون مشکل نیست. شاید اولین مشکل این روش این باشد، در صورتیکه در زمان فراخوانی، یکی از طرفین در دسترس نباشد، داده‌ها بروز رسانی نشوند و حل این مشکل پیچیدگی زیادی را برای شما بوجود بیاورد و دومین مشکل این است که در این روش نیز مانند روش قبل، با اضافه شدن به تعداد سیستم‌های وابسته شما مجبورید یا سرویس‌های جدیدی را برای این کار ایجاد کنید، یا سرویس‌های قبلی را تغییر دهید. تکرار این روال باعث می‌شود شما پس از مدتی با انبوهی از سرویس‌های مختلف رو برو شوید که مدیریت آنها برای شما بسیار دشوار است.

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

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

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

باید بگویم که این مشکل با استفاده از Message Broker حل می‌شود .

 یک  Message Broker  یک کامپوننت فیزیکی است که ارتباطات بین سیستمهای  مختلف را مدیریت می‌کند و درواقع برای حل مشکلات مرتبط با نحوه مدیریت ارتباطات و وابستگی‌های بین سیستم‌های مختلف طراحی شده است. با استفاده از Message Broker بجای اینکه سیستم‌های مختلف بصورت مستقیم با یکدیگر در ارتباط باشند، تنها با  Message Broker در ارتباط اند و در اینجا Message Broker نقش یک Interface را بصورتی ایفا می‌کند که وظیفه آن به حداقل رساندن وابستگی‌های مستقیم بین سیستم‌های مختلف است. همچنین  نحوه ارتباط قسمت‌های مختلف هم به این صورت است که یک سیستم با ارائه مشخصات گیرنده یا گیرندگان، یک Message را برای Message Broker  ارسال می‌کند و سپس Message Broker با روشها و الگوریتم‌هایی که در اختیار دارد و با جستجو در بین سیستم‌هایی که با آن مشخصات در آن ثبت شده‌اند، Message را برای آنها ارسال می‌کند.

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

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

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

لیست Message brokerها:

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

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

مطالب
نوشتن آزمون‌های واحد به کمک کتابخانه‌ی Moq - قسمت اول - معرفی
گاهی از اوقات، برای نوشتن آزمون‌های واحد، ایزوله سازی قسمتی که می‌خواهیم آن‌را بررسی کنیم، از سایر قسمت‌های سیستم مشکل می‌شود. برای مثال اگر در کلاسی کار اتصال به بانک اطلاعاتی صورت می‌گیرد و قصد داریم برای آن آزمون واحد بنویسیم، اما قرار نیست که الزاما با بانک اطلاعاتی کار کنیم، در این حالت نیاز به یک نمونه‌ی تقلیدی یا Mock از بانک اطلاعاتی را خواهیم داشت، تا کار دسترسی به بانک اطلاعاتی را شبیه سازی کند. در این سری با استفاده از کتابخانه‌ی بسیار معروف Moq (ماک‌یو تلفظ می‌شود؛ گاهی از اوقات هم ماک)، کار ایزوله سازی کلاس‌ها را انجام خواهیم داد، تا بتوانیم آن‌ها را مستقل از هم آزمایش کنیم.


Mocking چیست؟

فرض کنید برنامه‌ای را داریم که از تعدادی کلاس تشکیل شده‌است. در این بین می‌خواهیم تعدادی از آن‌ها را به صورت ایزوله‌ی از کل سیستم آزمایش کنیم. البته باید درنظر داشت که این کلاس‌ها در حین اجرای واقعی برنامه، از تعدادی وابستگی خاص در همان سیستم استفاده می‌کنند. برای مثال کلاسی در این بین برای بررسی میزان اعتبار مالی یک کاربر، نیاز دارد تا با یک وب سرویس خارجی کار کند. اما چون می‌خواهیم این کلاس را به صورت ایزوله‌ی از کل سیستم آزمایش کنیم، اینبار بجای استفاده‌ی از وابستگی واقعی این کلاس، آن وابستگی را با یک نمونه‌ی تقلیدی یا Mock object در اینجا، جایگزین می‌کنیم.
بنابراین Mocking به معنای جایگزین کردن یک وابستگی واقعی سیستم که در زمان اجرای آن مورد استفاده قرار می‌گیرد، با نمونه‌ی تقلیدی مختص زمان آزمایش برنامه، جهت بالابردن سهولت نوشتن آزمون‌های واحد است.


دلایل و مزایای استفاده‌ی از Mocking

- یکی از مهم‌ترین دلایل استفاده‌ی از Mocking، کاهش پیچیدگی تنظیمات اولیه‌ی نوشتن آزمون‌های واحد است. برای مثال اگر در برنامه‌ی خود از تزریق وابستگی‌ها استفاده می‌کنید و کلاسی دارای چندین وابستگی تزریق شده‌ی به آن است، برای آزمایش این کلاس نیاز به تدارک تمام این وابستگی‌ها را خواهید داشت تا بتوان این کلاس را وهله سازی کرد و همچنین برنامه را نیز کامپایل نمود. اما در این بین ممکن است آزمایش متدی در همان کلاس، الزاما از تمام وابستگی‌های تزریق شده‌ی در یک کلاس استفاده نکند. در این حالت، Mocking می‌تواند تنظیمات پیچیده‌ی وهله سازی این کلاس را به حداقل برساند.
- Mocking می‌تواند سبب افزایش سرعت اجرای آزمون‌های واحد نیز شود. برای مثال با تقلید سرویس‌های خارجی مورد استفاده‌ی در برنامه (هر عملی که از مرزهای سیستم رد شود مانند کار با شبکه، بانک اطلاعاتی، فایل سیستم و غیره)، می‌توان میزان I/O و همچنین زمان صرف شده‌ی به آن‌را به حداقل رساند.
- از mock objects می‌توان برای رهایی از مشکلات کار با مقادیر غیرمشخص استفاده کرد. برای مثال اگر در کدهای خود از DateTime.Now استفاده می‌کنید یا اعداد اتفاقی و امثال آن، هربار که آزمون‌های واحد را اجرا می‌کنیم، خروجی متفاوتی را دریافت کرده و بسیاری از آزمون‌های نوشته شده با مشکل مواجه می‌شوند. به کمک mocking می‌توان بجای این مقادیر غیرمشخص، یک مقدار ثابت و مشخص را بازگشت دهد.
- چون به سادگی می‌توان mock objects را تهیه کرد، می‌توان کار توسعه و آزمایش برنامه را پیش از به پایان رسیدن پیاده سازی اصلی سرویس‌های مدنظر، همینقدر که اینترفیس آن سرویس مشخص باشد، شروع کرد که می‌تواند برای کارهای تیمی بسیار مفید باشد.
- اگر وابستگی مورد استفاده ناپایدار و یا غیرقابل پیش بینی است، می‌توان توسط mocking به یک نمونه‌ی قابل پیش بینی و پایدار مخصوص آزمون‌های برنامه رسید.
- اگر وابستگی خارجی مورد استفاده به ازای هر بار استفاده، هزینه‌ای را شارژ می‌کند، می‌توان توسط mocking، هزینه‌ی آزمون‌های برنامه را کاهش داد.


Unit test چیست؟

بدیهی است در کنار آزمایش ایزوله‌ی قسمت‌های مختلف برنامه توسط mocking، باید کل برنامه را جهت بررسی دستیابی به نتایج واقعی نیز آزمایش کرد که به این نوع آزمون‌ها، آزمون یکپارچگی (Integration Tests)، API Tests ،UI Tests و غیره می‌گویند که در کنار Unit tests ما حضور خواهند داشت. بنابراین اکنون این سؤال مطرح می‌شود که یک Unit چیست؟
در برنامه‌ای که از چندین کلاس تشکیل می‌شود، به یک کلاس، یک Unit گفته می‌شود. همچنین اگر در این سیستم، دو یا چند کلاس با هم کار می‌کنند (کلاسی که از چندین وابستگی استفاده می‌کند)، این‌ها با هم نیز یک Unit را تشکیل دهند. بنابراین تعریف Unit بستگی به نحوه‌ی درک عملکرد یک سیستم و تعامل اجزای آن با هم دارد.


واژه‌های متناظر با Mock objects

در حین مطالعه‌ی منابع مرتبط با آزمون‌های واحد ممکن است با این واژه‌های تقریبا مشابه مواجه شوید: fakes ،stubs ،dummies و mocks. اما تفاوت آن‌ها در چیست؟
- Fakes در حقیقت یک نمونه پیاده سازی واقعی، اما غیرمناسب محیط واقعی و اصلی پروژه‌است. برای نمونه EF Core به همراه یک نمونه in-memory database هم هست که دقیقا با مفهوم Fakes تطابق دارد.
- از Dummies صرفا جهت تهیه‌ی پارامترهای مورد نیاز برای اجرای یک آزمایش استفاده می‌شوند. این پارامترها، هیچگاه در آزمایش‌های انجام شده مورد استفاده قرار نمی‌گیرند.
- از Stubs برای ارائه‌ی پاسخ‌هایی مشخص به فراخوان‌ها استفاده می‌شود. برای مثال یک متد یا خاصیت، دقیقا چه چیزی را باید بازگشت دهند.
- از Mocks برای بررسی تعامل اجزای مختلف در حال آزمایش استفاده می‌شود. آیا متدی یا خاصیتی مورد استفاده قرار گرفته‌است یا خیر؟

باید درنظر داشت که زمانیکه یک شیء Mock را توسط کتابخانه‌ی Moq تهیه می‌کنیم، هر سه مفهوم stubs ،dummies و mocks را با هم به همراه دارد. به همین جهت در این سری زمانیکه به یک mock object اشاره می‌شود، هر سه مفهوم مدنظر هستند.

واژه‌ی دیگری که ممکن است در این گروه زیاد مشاهده شود، «Test double» نام دارد که ترکیب هر 4 مورد fakes ،stubs ،dummies و mocks می‌باشد. در کل هر زمانیکه یک شیء مورد استفاده‌ی در زمان اجرای برنامه را جهت آزمایش ساده‌تر آن جایگزین می‌کنید، یک Test double را ایجاد کرده‌اید.


بررسی ساختار برنامه‌ای که می‌خواهیم آن‌را آزمایش کنیم

در این سری قصد داریم یک برنامه‌ی وام دهی را آزمایش کنیم که قسمت‌های مختلف آن دارای وابستگی‌های خاصی می‌باشند. ساختار این برنامه را در ادامه مشاهده می‌کنید:


موجودیت‌های برنامه‌ی وام دهی
namespace Loans.Entities
{
    public class Applicant
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public int Age { set; get; }

        public string Address { set; get; }

        public decimal Salary { set; get; }
    }
}

namespace Loans.Entities
{
    public class LoanProduct
    {
        public int Id { set; get; }

        public string ProductName { set; get; }

        public decimal InterestRate { set; get; }
    }
}

namespace Loans.Entities
{
    public class LoanApplication
    {
        public int Id { set; get; }

        public LoanProduct Product { set; get; }

        public LoanAmount Amount { set; get; }

        public Applicant Applicant { set; get; }

        public bool IsAccepted { set; get; }
    }

    public class LoanAmount
    {
        public string CurrencyCode { get; set; }

        public decimal Principal { get; set; }
    }
}

مدل‌های برنامه‌ی وام دهی

namespace Loans.Models
{
    public class IdentityVerificationStatus
    {
        public bool Passed { get; set; }
    }
}

سرویس‌های برنامه‌ی وام دهی

using Loans.Models;

namespace Loans.Services.Contracts
{
    public interface IIdentityVerifier
    {
        void Initialize();

        bool Validate(string applicantName, int applicantAge, string applicantAddress);

        void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid);

        void Validate(string applicantName, int applicantAge, string applicantAddress,
            ref IdentityVerificationStatus status);
    }
}

namespace Loans.Services.Contracts
{
    public interface ICreditScorer
    {
        int Score { get; }

        void CalculateScore(string applicantName, string applicantAddress);
    }
}

using System;
using Loans.Entities;
using Loans.Services.Contracts;

namespace Loans.Services
{
    public class LoanApplicationProcessor
    {
        private const decimal MinimumSalary = 1_500_000_0;
        private const int MinimumAge = 18;
        private const int MinimumCreditScore = 100_000;

        private readonly IIdentityVerifier _identityVerifier;
        private readonly ICreditScorer _creditScorer;

        public LoanApplicationProcessor(
            IIdentityVerifier identityVerifier,
            ICreditScorer creditScorer)
        {
            _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier));
            _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer));
        }

        public bool Process(LoanApplication application)
        {
            application.IsAccepted = false;

            if (application.Applicant.Salary < MinimumSalary)
            {
                return application.IsAccepted;
            }

            if (application.Applicant.Age < MinimumAge)
            {
                return application.IsAccepted;
            }

            _identityVerifier.Initialize();

            var isValidIdentity = _identityVerifier.Validate(
                application.Applicant.Name, application.Applicant.Age, application.Applicant.Address);

            if (!isValidIdentity)
            {
                return application.IsAccepted;
            }

            _creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address);
            if (_creditScorer.Score < MinimumCreditScore)
            {
                return application.IsAccepted;
            }

            application.IsAccepted = true;
            return application.IsAccepted;
        }
    }
}

using System;
using Loans.Models;
using Loans.Services.Contracts;

namespace Loans.Services
{
    public class IdentityVerifierServiceGateway : IIdentityVerifier
    {
        public DateTime LastCheckTime { get; private set; }

        public void Initialize()
        {
            // Initialize connection to external service
        }

        public bool Validate(string applicantName, int applicantAge, string applicantAddress)
        {
            Connect();
            var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress);
            LastCheckTime = DateTime.Now;
            Disconnect();

            return isValidIdentity;
        }

        private void Connect()
        {
            // Open connection to external service
        }

        private bool CallService(string applicantName, int applicantAge, string applicantAddress)
        {
            // Make call to external service, interpret the response, and return result

            return false; // Simulate result for demo purposes
        }

        private void Disconnect()
        {
            // Close connection to external service
        }

        public void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid)
        {
            throw new NotImplementedException();
        }

        public void Validate(string applicantName, int applicantAge, string applicantAddress,
            ref IdentityVerificationStatus status)
        {
            throw new NotImplementedException();
        }
    }
}
توضیحات:
هدف از این برنامه، درخواست یک وام جدید است. Application در اینجا به معنای درخواست یا فرم جدید است و Applicant نیز شخصی است که این درخواست را داده‌است.
در اینجا بیشتر تمرکز ما بر روی کلاس LoanApplicationProcessor است که دارای دو وابستگی تزریق شده‌ی به آن نیز می‌باشد:
        public LoanApplicationProcessor(
            IIdentityVerifier identityVerifier,
            ICreditScorer creditScorer)
        {
            _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier));
            _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer));
        }
از این وابستگی‌ها برای تصدیق هویت درخواست کننده و همچنین بررسی میزان اعتبار او استفاده می‌شود.
تمام این منطق نیز در متد Process آن قابل مشاهده‌است که هدف اصلی آن، بررسی قابل پذیرش بودن درخواست یک وام جدید است.


نوشتن اولین تست، برای برنامه‌ی وام دهی

در اولین تصویر این قسمت، پروژه‌ی class library دومی را نیز به نام Loans.Tests مشاهده می‌کنید. فایل csproj آن به صورت زیر برای کار با MSTest تنظیم شده‌است:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\Loans\Loans.csproj" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.3.0" />
    <PackageReference Include="MSTest.TestAdapter" Version="2.0.0" />
    <PackageReference Include="MSTest.TestFramework" Version="2.0.0" />    
  </ItemGroup>
</Project>
که در آن ارجاعی به پروژه‌ی Loans.csproj و همچنین وابستگی‌های MSTest، تنظیم شده‌اند.

اکنون اولین آزمون واحد ما در کلاس جدید LoanApplicationProcessorShould چنین شکلی را پیدا می‌کند:
using Loans.Entities;
using Loans.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Loans.Tests
{
    [TestClass]
    public class LoanApplicationProcessorShould
    {
        [TestMethod]
        public void DeclineLowSalary()
        {
            var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m};
            var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0};
            var applicant =
                new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_100_000_0};
            var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant};
            var processor = new LoanApplicationProcessor(null, null);
            processor.Process(application);

            Assert.IsFalse(application.IsAccepted);
        }
    }
}
در حین کار با MSTest، کلاس آزمون واحد باید به ویژگی TestClass و متدهای public void آن به ویژگی TestMethod مزین شوند تا توسط این فریم‌ورک آزمون واحد شناسایی شده و مورد آزمایش قرار گیرند.
در این آزمایش، شخص درخواست کننده، حقوق کمی دارد و می‌خواهیم بررسی کنیم که آیا LoanApplicationProcessor می‌تواند آن‌را بر اساس مقدار MinimumSalary، رد کند یا خیر؟
public class LoanApplicationProcessor
{
    private const decimal MinimumSalary = 1_500_000_0;

در حین وهله سازی LoanApplicationProcessor، دو وابستگی آن به null تنظیم شده‌اند؛ چون می‌دانیم که بررسی MinimumSalary پیش از سایر بررسی‌ها صورت می‌گیرد و اساسا در این آزمایش، نیازی به این وابستگی‌ها نداریم.
اما اگر سعی در اجرای این آزمایش کنیم (برای مثال با اجرای دستور dotnet test در خط فرمان)، آزمایش اجرا نشده و با استثنای زیر مواجه می‌شویم:
Test method Loans.Tests.LoanApplicationProcessorShould.DeclineLowSalary threw exception:
System.ArgumentNullException: Value cannot be null.
Parameter name: identityVerifier
چون در سازنده‌ی کلاس LoanApplicationProcessor، در صورت نال بودن وابستگی‌های دریافتی، یک استثناء صادر می‌شود. بنابراین ذکر آن‌ها الزامی است:
        public LoanApplicationProcessor(
            IIdentityVerifier identityVerifier,
            ICreditScorer creditScorer)
        {
            _identityVerifier = identityVerifier ?? throw new ArgumentNullException(nameof(identityVerifier));
            _creditScorer = creditScorer ?? throw new ArgumentNullException(nameof(creditScorer));
        }


نصب کتابخانه‌ی Moq جهت برآورده کردن وابستگی‌های کلاس LoanApplicationProcessor

در این آزمایش چون وجود وابستگی‌های در سازنده‌ی کلاس، برای ما اهمیتی ندارند و همچنین ذکر آن‌ها نیز الزامی است، می‌خواهیم توسط کتابخانه‌ی Moq، دو نمونه‌ی تقلیدی از آن‌ها را تهیه کرده (همان dummies که پیشتر معرفی شدند) و جهت برآورده کردن بررسی صورت گرفته‌ی در سازنده‌ی کلاس LoanApplicationProcessor، آن‌ها را ارائه کنیم.
کتابخانه‌ی بسیار معروف Moq، با پروژه‌های مبتنی بر NETFramework 4.5. و همچنین NETStandard 2.0. به بعد سازگار است و برای نصب آن، می‌توان یکی از دو دستور زیر را صادر کرد:
> dotnet add package Moq
> Install-Package Moq

اما چرا کتابخانه‌ی Moq؟
کتابخانه‌ی Moq این اهداف را دنبال می‌کند: ساده‌است، به شدت کاربردی‌است و همچنین strongly typed است. این کتابخانه سورس باز بوده و تعداد بار دانلود بسته‌ی نیوگت آن میلیونی است.


پس از نصب آن، اولین آزمایشی را که نوشتیم، به صورت زیر اصلاح می‌کنیم:
using Loans.Entities;
using Loans.Services;
using Loans.Services.Contracts;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Loans.Tests
{
    [TestClass]
    public class LoanApplicationProcessorShould
    {
        [TestMethod]
        public void DeclineLowSalary()
        {
            var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m};
            var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0};
            var applicant =
                new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_100_000_0};
            var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant};

            var mockIdentityVerifier = new Mock<IIdentityVerifier>();
            var mockCreditScorer = new Mock<ICreditScorer>();

            var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object);
            processor.Process(application);

            Assert.IsFalse(application.IsAccepted);
        }
    }
}
در اینجا بجای ارسال null به سازنده‌ی کلاس LoanApplicationProcessor، جهت برآورده کردن مقدار پیش‌فرض پارامترهای آن و کامپایل شدن برنامه، نمونه‌های تقلیدی دو وابستگی مورد نیاز آن‌را تهیه و به آن ارسال کرده‌ایم.
کار با ذکر new Mock شروع شده و آرگومان جنریک آن‌را از نوع وابستگی‌هایی که نیاز داریم، مقدار دهی می‌کنیم. سپس خاصیت Object آن، امکان دسترسی به این شیء تقلید شده را میسر می‌کند.
اکنون اگر مجددا این آزمون واحد را اجرا کنیم، مشاهده خواهیم کرد که بجای صدور استثناء، با موفقیت به پایان رسیده‌است:



گاهی از اوقات جایگزین کردن یک وابستگی null با نمونه‌ی Mock آن کافی نیست

در مثالی که بررسی کردیم، اشیاء mock، کار برآورده کردن نیازهای ابتدایی آزمایش را انجام داده و سبب اجرای موفقیت آمیز آن شدند؛ اما همیشه اینطور نیست:
using Loans.Entities;
using Loans.Services;
using Loans.Services.Contracts;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Loans.Tests
{
    [TestClass]
    public class LoanApplicationProcessorShould
    {        
        [TestMethod]
        public void Accept()
        {
            var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m};
            var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0};
            var applicant =
                new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0};
            var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant};

            var mockIdentityVerifier = new Mock<IIdentityVerifier>();
            var mockCreditScorer = new Mock<ICreditScorer>();

            var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object);
            processor.Process(application);

            Assert.IsTrue(application.IsAccepted);
        }
    }
}
تفاوت این آزمایش جدید با قبلی، در دو مورد است: مقدار Salary به MinimumSalary تنظیم شده‌است و در آخر Assert.IsTrue را داریم.
اگر این آزمایش را اجرا کنیم، با شکست مواجه خواهد شد. علت اینجا است که هرچند در حال استفاده‌ی از دو mock object به عنوان وابستگی‌های مورد نیاز هستیم، اما تنظیمات خاصی را بر روی آن‌ها انجام نداده‌ایم و به همین جهت خروجی مناسبی را در اختیار LoanApplicationProcessor قرار نمی‌دهند. برای مثال مرحله‌ی بعدی بررسی اعتبار شخص در کلاس LoanApplicationProcessor، فراخوانی سرویس identityVerifier و متد Validate آن است که خروجی آن بر اساس کدهای فعلی، همیشه false است:
_identityVerifier.Initialize();
var isValidIdentity = _identityVerifier.Validate(
    application.Applicant.Name, application.Applicant.Age, application.Applicant.Address);
در قسمت بعدی، کار تنظیم اشیاء mock را انجام خواهیم داد.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MoqSeries-01.zip
مطالب
ASP.NET MVC و Identity 2.0 : مفاهیم پایه
در تاریخ 20 مارچ 2014 تیم ASP.NET نسخه نهایی Identity 2.0 را منتشر کردند . نسخه جدید برخی از ویژگی‌های درخواست شده پیشین را عرضه می‌کند و در کل قابلیت‌های احراز هویت و تعیین سطح دسترسی ارزنده ای را پشتیبانی می‌کند. این فریم ورک در تمام اپلیکیشن‌های ASP.NET می‌تواند بکار گرفته شود.

فریم ورک Identity در سال 2013 معرفی شد، که دنباله سیستم ASP.NET Membership بود. سیستم قبلی گرچه طی سالیان استفاده می‌شد اما مشکلات زیادی هم بهمراه داشت. بعلاوه با توسعه دنیای وب و نرم افزار، قابلیت‌های مدرنی مورد نیاز بودند که باید پشتیبانی می‌شدند. فریم ورک Identity در ابتدا سیستم ساده و کارآمدی برای مدیریت کاربران بوجود آورد و مشکلات پیشین را تا حد زیادی برطرف نمود. بعنوان مثال فریم ورک جدید مبتنی بر EF Code-first است، که سفارشی کردن سیستم عضویت را بسیار آسان می‌کند و به شما کنترل کامل می‌دهد. یا مثلا احراز هویت مبتنی بر پروتوکل OAuth پشتیبانی می‌شود که به شما اجازه استفاده از فراهم کنندگان خارجی مانند گوگل، فیسبوک و غیره را می‌دهد.

نسخه جدید این فریم ورک ویژگی‌های زیر را معرفی می‌کند (بعلاوه مواردی دیگر):

  • مدل حساب‌های کاربری توسعه داده شده. مثلا آدرس ایمیل و اطلاعات تماس را هم در بر می‌گیرد
  • احراز هویت دو مرحله ای (Two-Factor Authentication) توسط اطلاع رسانی ایمیلی یا پیامکی. مشابه سیستمی که گوگل، مایکروسافت و دیگران استفاده می‌کنند
  • تایید حساب‌های کاربری توسط ایمیل (Account Confirmation)
  • مدیریت کاربران و نقش‌ها (Administration of Users & Roles)
  • قفل کردن حساب‌های کاربری در پاسخ به Invalid log-in attempts
  • تامین کننده شناسه امنیتی (Security Token Provider) برای بازتولید شناسه‌ها در پاسخ به تغییرات تنظیمات امنیتی (مثلا هنگام تغییر کلمه عبور)
  • بهبود پشتیبانی از Social log-ins
  • یکپارچه سازی ساده با Claims-based Authorization

Identity 2.0 تغییرات چشم گیری نسبت به نسخه قبلی به‌وجود آورده است. به نسبت ویژگی‌های جدید، پیچیدگی‌هایی نیز معرفی شده‌اند. اگر به تازگی (مانند خودم) با نسخه 1 این فریم ورک آشنا شده و کار کرده اید، آماده شوید! گرچه لازم نیست از صفر شروع کنید، اما چیزهای بسیاری برای آموختن وجود دارد.

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

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

در این مقاله با مقدار قابل توجهی کد مواجه خواهید شد. لازم نیست تمام جزئیات آنها را بررسی کنید، تنها با ساختار کلی این فریم ورک آشنا شوید. کامپوننت‌ها را بشناسید و بدانید که هر کدام در کجا قرار گرفته اند، چطور کار می‌کنند و اجزای کلی سیستم چگونه پیکربندی می‌شوند. گرچه، اگر به برنامه نویسی دات نت (#ASP.NET, C) تسلط دارید و با نسخه قبلی Identity هم کار کرده اید، درک کدهای جدید کار ساده ای خواهد بود.

Identity 2.0 با نسخه قبلی سازگار نیست

اپلیکیشن هایی که با نسخه 1.0 این فریم ورک ساخته شده اند نمی‌توانند بسادگی به نسخه جدید مهاجرت کنند. قابلیت هایی جدیدی که پیاده سازی شده اند تغییرات چشمگیری در معماری این فریم ورک بوجود آورده اند، همچنین API مورد استفاده در اپلیکیشن‌ها نیز دستخوش تغییراتی شده است. مهاجرت از نسخه 1.0 به 2.0 نیاز به نوشتن کدهای جدید و اعمال تغییرات متعددی دارد که از حوصله این مقاله خارج است. فعلا همین قدر بدانید که این مهاجرت نمی‌تواند بسادگی در قالب Plug-in and play صورت پذیرد!

شروع به کار : پروژه مثال‌ها را از NuGet دریافت کنید

در حال حاظر (هنگام نوشتن این مقاله) قالب پروژه استانداردی برای اپلیکیشن‌های ASP.NET MVC که ا ز Identity 2.0 استفاده کنند وجود ندارد. برای اینکه بتوانید از نسخه جدید این فریم ورک استفاده کنید، باید پروژه مثال را توسط NuGet دریافت کنید. ابتدا پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب Empty را در دیالوگ تنظیمات انتخاب کنید.

کنسول Package Manager را باز کنید و با اجرای فرمان زیر پروژه مثال‌ها را دانلود کنید.

PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
پس از آنکه NuGet کار خود را به اتمام رساند، ساختار پروژه ای مشابه پروژه‌های استاندارد MVC مشاهده خواهید کرد. پروژه شما شامل قسمت‌های Models, Views, Controllers و کامپوننت‌های دیگری برای شروع به کار است. گرچه در نگاه اول ساختار پروژه بسیار شبیه به پروژه‌های استاندارد ASP.NET MVC به نظر می‌آید، اما با نگاهی دقیق‌تر خواهید دید که تغییرات جدیدی ایجاد شده‌اند و پیچیدگی هایی نیز معرفی شده اند.

پیکربندی Identity : دیگر به سادگی نسخه قبلی نیست
به نظر من یکی از مهم‌ترین نقاط قوت فریم ورک Identity یکی از مهم‌ترین نقاط ضعفش نیز بود. سادگی نسخه 1.0 این فریم ورک کار کردن با آن را بسیار آسان می‌کرد و به سادگی می‌توانستید ساختار کلی و روند کارکردن کامپوننت‌های آن را درک کنید. اما همین سادگی به معنای محدود بودن امکانات آن نیز بود. بعنوان مثال می‌توان به تایید حساب‌های کاربری یا پشتیبانی از احراز هویت‌های دو مرحله ای اشاره کرد.

برای شروع نگاهی اجمالی به پیکربندی این فریم ورک و اجرای اولیه اپلیکیشن خواهیم داشت. سپس تغییرات را با نسخه 1.0 مقایسه می‌کنیم.

در هر دو نسخه، فایلی بنام Startup.cs در مسیر ریشه پروژه خواهید یافت. در این فایل کلاس واحدی بنام Startup تعریف شده است که متد ()ConfigureAuth را فراخوانی می‌کند. چیزی که در این فایل مشاهده نمی‌کنیم، خود متد ConfigureAuth است. این بدین دلیل است که مابقی کد کلاس Startup در یک کلاس پاره ای (Partial) تعریف شده که در پوشه App_Start قرار دارد. نام فایل مورد نظر Startup.Auth.cs است که اگر آن را باز کنید تعاریف یک کلاس پاره ای بهمراه متد ()ConfigureAuth را خواهید یافت. در یک پروژه که از نسخه Identity 1.0 استفاده می‌کند، کد متد ()ConfigureAuth مطابق لیست زیر است.
public partial class Startup
{
    public void ConfigureAuth(IAppBuilder app)
    {
        // Enable the application to use a cookie to 
        // store information for the signed in user
        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login")
        });

        // Use a cookie to temporarily store information about a 
        // user logging in with a third party login provider
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Uncomment the following lines to enable logging 
        // in with third party login providers
        //app.UseMicrosoftAccountAuthentication(
        //    clientId: "",
        //    clientSecret: "");

        //app.UseTwitterAuthentication(
        //   consumerKey: "",
        //   consumerSecret: "");
 
        //app.UseFacebookAuthentication(
        //   appId: "",
        //   appSecret: "");

        //app.UseGoogleAuthentication();
    }
}
در قطعه کد بالا پیکربندی لازم برای کوکی‌ها را مشاهده می‌کنید. همچنین کدهایی بصورت توضیحات وجود دارد که به منظور استفاده از تامین کنندگان خارجی مانند گوگل، فیسبوک، توییتر و غیره استفاده می‌شوند. حال اگر به کد این متد در نسخه Identity 2.0 دقت کنید خواهید دید که کد بیشتری نوشته شده است.
public partial class Startup {
    public void ConfigureAuth(IAppBuilder app) {
        // Configure the db context, user manager and role 
        // manager to use a single instance per request
        app.CreatePerOwinContext(ApplicationDbContext.Create);
        app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
        app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);

        // Enable the application to use a cookie to store information for the 
        // signed in user and to use a cookie to temporarily store information 
        // about a user logging in with a third party login provider 
        // Configure the sign in cookie
        app.UseCookieAuthentication(new CookieAuthenticationOptions {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider {
                // Enables the application to validate the security stamp when the user 
                // logs in. This is a security feature which is used when you 
                // change a password or add an external login to your account.  
                OnValidateIdentity = SecurityStampValidator
                    .OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromMinutes(30),
                    regenerateIdentity: (manager, user) 
                    => user.GenerateUserIdentityAsync(manager))
            }
        });

        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

        // Enables the application to temporarily store user information when 
        // they are verifying the second factor in the two-factor authentication process.
        app.UseTwoFactorSignInCookie(
            DefaultAuthenticationTypes.TwoFactorCookie, 
            TimeSpan.FromMinutes(5));

        // Enables the application to remember the second login verification factor such 
        // as phone or email. Once you check this option, your second step of 
        // verification during the login process will be remembered on the device where 
        // you logged in from. This is similar to the RememberMe option when you log in.
        app.UseTwoFactorRememberBrowserCookie(
            DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);

        // Uncomment the following lines to enable logging in 
        // with third party login providers
        //app.UseMicrosoftAccountAuthentication(
        //    clientId: "",
        //    clientSecret: "");

        //app.UseTwitterAuthentication(
        //   consumerKey: "",
        //   consumerSecret: "");

        //app.UseFacebookAuthentication(
        //   appId: "",
        //   appSecret: "");

        //app.UseGoogleAuthentication();
    }
}
اول از همه به چند فراخوانی متد app.CreatePerOwinContext بر می‌خوریم. با این فراخوانی‌ها Callback هایی را رجیستر می‌کنیم که آبجکت‌های مورد نیاز را بر اساس نوع تعریف شده توسط type arguments وهله سازی می‌کنند. این وهله‌ها سپس توسط فراخوانی متد ()context.Get قابل دسترسی خواهند بود. این به ما می‌گوید که حالا Owin بخشی از اپلیکیشن ما است و فریم ورک Identity 2.0 از آن برای ارائه قابلیت هایش استفاده می‌کند.

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

تا اینجا پیکربندی‌های اساسی برای اپلیکیشن شما انجام شده است و می‌توانید از اپلیکیشن خود استفاده کنید. بکارگیری فراهم کنندگان خارجی در حال حاضر غیرفعال است و بررسی آنها نیز از حوصله این مقاله خارج است. این کلاس پیکربندی‌های اساسی Identity را انجام می‌دهد. کامپوننت‌های پیکربندی و کدهای کمکی دیگری نیز وجود دارند که در کلاس IdentityConfig.cs تعریف شده اند.

پیش از آنکه فایل IdentityConfig.cs را بررسی کنیم، بهتر است نگاهی به کلاس ApplicationUser بیاندازیم که در پوشه Models قرار گرفته است.


کلاس جدید ApplicationUser در Identity 2.0
اگر با نسخه 1.0 این فریم ورک اپلیکیشنی ساخته باشید، ممکن است متوجه شده باشید که کلاس پایه IdentityUser محدود و شاید ناکافی باشد. در نسخه قبلی، این فریم ورک پیاده سازی IdentityUser را تا حد امکان ساده نگاه داشته بود تا اطلاعات پروفایل کاربران را معرفی کند.
public class IdentityUser : IUser
{
    public IdentityUser();
    public IdentityUser(string userName);

    public virtual string Id { get; set; }
    public virtual string UserName { get; set; }

    public virtual ICollection<IdentityUserRole> Roles { get; }
    public virtual ICollection<IdentityUserClaim> Claims { get; }
    public virtual ICollection<IdentityUserLogin> Logins { get; }

    public virtual string PasswordHash { get; set; }
    public virtual string SecurityStamp { get; set; }
}
بین خواص تعریف شده در این کلاس، تنها Id, UserName و Roles برای ما حائز اهمیت هستند (از دید برنامه نویسی). مابقی خواص عمدتا توسط منطق امنیتی این فریم ورک استفاده می‌شوند و کمک شایانی در مدیریت اطلاعات کاربران به ما نمی‌کنند.

اگر از نسخه Identity 1.0 استفاده کرده باشید و مطالعاتی هم در این زمینه داشته باشید، می‌دانید که توسعه کلاس کاربران بسیار ساده است. مثلا برای افزودن فیلد آدرس ایمیل و اطلاعات دیگر کافی بود کلاس ApplicationUser را ویرایش کنیم و از آنجا که این فریم ورک مبتنی بر EF Code-first است بروز رسانی دیتابیس و مابقی اپلیکیشن کار چندان مشکلی نخواهد بود.

با ظهور نسخه Identity 2.0 نیاز به برخی از این سفارشی سازی‌ها از بین رفته است. گرچه هنوز هم می‌توانید بسادگی مانند گذشته کلاس ApplicationUser را توسعه و گسترش دهید، تیم ASP.NET تغییراتی بوجود آورده اند تا نیازهای رایج توسعه دهندگان را پاسخگو باشد.

اگر به کد کلاس‌های مربوطه دقت کنید خواهید دید که کلاس ApplicationUser همچنان از کلاس پایه IdentityUser ارث بری می‌کند، اما این کلاس پایه پیچیده‌تر شده است. کلاس ApplicationUser در پوشه Models و در فایلی بنام IdentityModels.cs تعریف شده است. همانطور که می‌بینید تعاریف خود این کلاس بسیار ساده است.
public class ApplicationUser : IdentityUser {
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(
        UserManager<ApplicationUser> manager) {
        // Note the authenticationType must match the one 
        // defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = 
            await manager.CreateIdentityAsync(this, 
                DefaultAuthenticationTypes.ApplicationCookie);

        // Add custom user claims here
        return userIdentity;
    }
}
حال اگر تعاریف کلاس IdentityUser را بازیابی کنید (با استفاده از قابلیت Go To Definition در VS) خواهید دید که این کلاس خود از کلاس پایه دیگری ارث بری می‌کند. اگر به این پیاده سازی دقت کنید کاملا واضح است که ساختار این کلاس به کلی نسبت به نسخه قبلی تغییر کرده است.
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
    where TLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey>
    where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey>
    where TClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey>
{
    public IdentityUser();

    // Used to record failures for the purposes of lockout
    public virtual int AccessFailedCount { get; set; }
    // Navigation property for user claims
    public virtual ICollection<TClaim> Claims { get; }
    // Email
    public virtual string Email { get; set; }
    // True if the email is confirmed, default is false
    public virtual bool EmailConfirmed { get; set; }
    // User ID (Primary Key)
    public virtual TKey Id { get; set; }
    // Is lockout enabled for this user
    public virtual bool LockoutEnabled { get; set; }
    // DateTime in UTC when lockout ends, any 
    // time in the past is considered not locked out.
    public virtual DateTime? LockoutEndDateUtc { get; set; }

    // Navigation property for user logins
    public virtual ICollection<TLogin> Logins { get; }
    // The salted/hashed form of the user password
    public virtual string PasswordHash { get; set; }
    // PhoneNumber for the user
    public virtual string PhoneNumber { get; set; }
    // True if the phone number is confirmed, default is false
    public virtual bool PhoneNumberConfirmed { get; set; }
    // Navigation property for user roles
    public virtual ICollection<TRole> Roles { get; }

    // A random value that should change whenever a users 
    // credentials have changed (password changed, login removed)
    public virtual string SecurityStamp { get; set; }
    // Is two factor enabled for the user
    public virtual bool TwoFactorEnabled { get; set; }
    // User name
    public virtual string UserName { get; set; }
}
اول از همه آنکه برخی از خواص تعریف شده هنوز توسط منطق امنیتی فریم ورک استفاده می‌شوند و از دید برنامه نویسی مربوط به مدیریت اطلاعات کاربران نیستند. اما به هر حال فیلد‌های Email و PhoneNumber نیاز به ویرایش تعریف پیش فرض موجودیت کاربران را از بین می‌برد.

اما از همه چیز مهم‌تر امضا (Signature)ی خود کلاس است. این آرگومانهای جنریک چه هستند؟ به امضای این کلاس دقت کنید.
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey>
    where TLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey>
    where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey>
    where TClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey>
نسخه جدید IdentityUser انواع آرگومانهای جنریک را پیاده سازی می‌کند که انعطاف پذیری بسیار بیشتری به ما می‌دهند. بعنوان مثال بیاد بیاورید که نوع داده فیلد Id در نسخه 1.0 رشته (string) بود. اما در نسخه جدید استفاده از آرگومانهای جنریک (در اینجا TKey) به ما اجازه می‌دهد که نوع این فیلد را مشخص کنیم. همانطور که مشاهده می‌کنید خاصیت Id در این کلاس نوع داده TKey را باز می‌گرداند.
public virtual TKey Id { get; set; }
یک مورد حائز اهمیت دیگر خاصیت Roles است که بصورت زیر تعریف شده.
public virtual ICollection<TRole> Roles { get; }
همانطور که می‌بینید نوع TRole بصورت جنریک تعریف شده و توسط تعاریف کلاس IdentityUser مشخص می‌شود. اگر به محدودیت‌های پیاده سازی این خاصیت دقت کنید می‌بینید که نوع این فیلد به <IdentityUserRole<TKey محدود (constraint) شده است، که خیلی هم نسبت به نسخه 1.0 تغییری نکرده. چیزی که تفاوت چشمگیری کرده و باعث breaking changes می‌شود تعریف خود IdentityUserRole است.

در نسخه Identity 1.0 کلاس IdentityUserRole بصورت زیر تعریف شده بود.
public class IdentityUserRole 
{
      public IdentityUserRole();
      public virtual IdentityRole Role { get; set; }
      public virtual string RoleId { get; set; }
      public virtual IdentityUser User { get; set; }
      public virtual string UserId { get; set; }
}
این کلاس را با پیاده سازی نسخه Identity 2.0 مقایسه کنید.
public class IdentityUserRole<TKey> 
{
    public IdentityUserRole();
    public virtual TKey RoleId { get; set; }
    public virtual TKey UserId { get; set; }
}
پیاده سازی پیشین ارجاعاتی به آبجکت هایی از نوع IdentityRole و IdentityUser داشت. پیاده سازی نسخه 2.0 تنها شناسه‌ها را ذخیره می‌کند. اگر در اپلیکیشنی که از نسخه 1.0 استفاده می‌کند سفارشی سازی هایی انجام داده باشید (مثلا تعریف کلاس Role را توسعه داده باشید، یا سیستمی مانند Group-based Roles را پیاده سازی کرده باشید) این تغییرات سیستم قبلی شما را خواهد شکست.

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

از آنجا که کلاس ApplicationUser از IdentityUser ارث بری می‌کند، تمام خواص و تعاریف این کلاس پایه در ApplicationUser قابل دسترسی هستند.


کامپوننت‌های پیکربندی Identity 2.0 و کدهای کمکی
گرچه متد ()ConfigAuth در کلاس Startup، محلی است که پیکربندی Identity در زمان اجرا صورت می‌پذیرد، اما در واقع کامپوننت‌های موجود در فایل IdentityConfig.cs هستند که اکثر قابلیت‌های Identity 2.0 را پیکربندی کرده و نحوه رفتار آنها در اپلیکیشن ما را کنترل می‌کنند.

اگر محتوای فایل IdentityConfig.cs را بررسی کنید خواهید دید که کلاس‌های متعددی در این فایل تعریف شده اند. می‌توان تک تک این کلاس‌ها را به فایل‌های مجزایی منتقل کرد، اما برای مثال جاری کدها را بهمین صورت رها کرده و نگاهی اجمالی به آنها خواهیم داشت. بهرحال در حال حاظر تمام این کلاس‌ها در فضای نام ApplicationName.Models قرار دارند.


Application User Manager و Application Role Manager
اولین چیزی که در این فایل به آنها بر می‌خوریم دو کلاس ApplicationUserManager و ApplicationRoleManager هستند. آماده باشید، مقدار زیادی کد با انواع داده جنریک در پیش روست!
public class ApplicationUserManager : UserManager<ApplicationUser>
{
    public ApplicationUserManager(IUserStore<ApplicationUser> store)
        : base(store)
    {
    }

    public static ApplicationUserManager Create(
        IdentityFactoryOptions<ApplicationUserManager> options, 
        IOwinContext context)
    {
        var manager = new ApplicationUserManager(
            new UserStore<ApplicationUser>(
                context.Get<ApplicationDbContext>()));

        // Configure validation logic for usernames
        manager.UserValidator = 
            new UserValidator<ApplicationUser>(manager)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = true
        };

        // Configure validation logic for passwords
        manager.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 6, 
            RequireNonLetterOrDigit = true,
            RequireDigit = true,
            RequireLowercase = true,
            RequireUppercase = true,
        };

        // Configure user lockout defaults
        manager.UserLockoutEnabledByDefault = true;
        manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
        manager.MaxFailedAccessAttemptsBeforeLockout = 5;

        // Register two factor authentication providers. This application uses 
        // Phone and Emails as a step of receiving a code for verifying 
        // the user You can write your own provider and plug in here.
        manager.RegisterTwoFactorProvider("PhoneCode", 
            new PhoneNumberTokenProvider<ApplicationUser>
        {
            MessageFormat = "Your security code is: {0}"
        });

        manager.RegisterTwoFactorProvider("EmailCode", 
            new EmailTokenProvider<ApplicationUser>
        {
            Subject = "SecurityCode",
            BodyFormat = "Your security code is {0}"
        });

        manager.EmailService = new EmailService();
        manager.SmsService = new SmsService();

        var dataProtectionProvider = options.DataProtectionProvider;

        if (dataProtectionProvider != null)
        {
            manager.UserTokenProvider = 
                new DataProtectorTokenProvider<ApplicationUser>(
                    dataProtectionProvider.Create("ASP.NET Identity"));
        }

        return manager;
    }

    public virtual async Task<IdentityResult> AddUserToRolesAsync(
        string userId, IList<string> roles)
    {
        var userRoleStore = (IUserRoleStore<ApplicationUser, string>)Store;
        var user = await FindByIdAsync(userId).ConfigureAwait(false);

        if (user == null)
        {
            throw new InvalidOperationException("Invalid user Id");
        }

        var userRoles = await userRoleStore
            .GetRolesAsync(user)
            .ConfigureAwait(false);

        // Add user to each role using UserRoleStore
        foreach (var role in roles.Where(role => !userRoles.Contains(role)))
        {
            await userRoleStore.AddToRoleAsync(user, role).ConfigureAwait(false);
        }

        // Call update once when all roles are added
        return await UpdateAsync(user).ConfigureAwait(false);
    }

    public virtual async Task<IdentityResult> RemoveUserFromRolesAsync(
        string userId, IList<string> roles)
    {
        var userRoleStore = (IUserRoleStore<ApplicationUser, string>) Store;
        var user = await FindByIdAsync(userId).ConfigureAwait(false);

        if (user == null)
        {
            throw new InvalidOperationException("Invalid user Id");
        }

        var userRoles = await userRoleStore
            .GetRolesAsync(user)
            .ConfigureAwait(false);

        // Remove user to each role using UserRoleStore
        foreach (var role in roles.Where(userRoles.Contains))
        {
            await userRoleStore
                .RemoveFromRoleAsync(user, role)
                .ConfigureAwait(false);
        }

        // Call update once when all roles are removed
        return await UpdateAsync(user).ConfigureAwait(false);
    }
}
قطعه کد بالا گرچه شاید به نظر طولانی بیاید، اما در کل، کلاس ApplicationUserManager توابع مهمی را برای مدیریت کاربران فراهم می‌کند: ایجاد کاربران جدید، انتساب کاربران به نقش‌ها و حذف کاربران از نقش ها. این کلاس بخودی خود از کلاس <UserManager<ApplicationUser ارث بری می‌کند بنابراین تمام قابلیت‌های UserManager در این کلاس هم در دسترس است. اگر به متد ()Create دقت کنید می‌بینید که وهله ای از نوع ApplicationUserManager باز می‌گرداند. بیشتر تنظیمات پیکربندی و تنظیمات پیش فرض کاربران شما در این متد اتفاق می‌افتد.

مورد حائز اهمیت بعدی در متد ()Create فراخوانی ()<context.Get<ApplicationDBContext است. بیاد بیاورید که پیشتر نگاهی به متد ()ConfigAuth داشتیم که چند فراخوانی CreatePerOwinContext داشت که توسط آنها Callback هایی را رجیستر می‌کردیم. فراخوانی متد ()<context.Get<ApplicationDBContext این Callback‌‌ها را صدا می‌زند، که در اینجا فراخوانی متد استاتیک ()ApplicationDbContext.Create خواهد بود. در ادامه بیشتر درباره این قسمت خواهید خواهند.

اگر دقت کنید می‌بینید که احراز هویت، تعیین سطوح دسترسی و تنظیمات مدیریتی و مقادیر پیش فرض آنها در متد ()Create انجام می‌شوند و سپس وهله ای از نوع خود کلاس ApplicationUserManager بازگشت داده می‌شود. همچنین سرویس‌های احراز هویت دو مرحله ای نیز در همین مرحله پیکربندی می‌شوند. اکثر پیکربندی‌ها و تنظیمات نیازی به توضیح ندارند و قابل درک هستند. اما احراز هویت دو مرحله ای نیاز به بررسی عمیق‌تری دارد. در ادامه به این قسمت خواهیم پرداخت. اما پیش از آن نگاهی به کلاس ApplicationRoleManager بیاندازیم.
public class ApplicationRoleManager : RoleManager<IdentityRole>
{
    public ApplicationRoleManager(IRoleStore<IdentityRole,string> roleStore)
        : base(roleStore)
    {
    }

    public static ApplicationRoleManager Create(
        IdentityFactoryOptions<ApplicationRoleManager> options, 
        IOwinContext context)
    {
        var manager = new ApplicationRoleManager(
            new RoleStore<IdentityRole>(
                context.Get<ApplicationDbContext>()));

        return manager;
    }
}
مانند کلاس ApplicationUserManager مشاهده می‌کنید که کلاس ApplicationRoleManager از <RoleManager<IdentityRole ارث بری می‌کند. بنابراین تمام قابلیت‌های کلاس پایه نیز در این کلاس در دسترس هستند. یکبار دیگر متدی بنام ()Create را مشاهده می‌کنید که وهله ای از نوع خود کلاس بر می‌گرداند.


سرویس‌های ایمیل و پیامک برای احراز هویت دو مرحله ای و تایید حساب‌های کاربری
دو کلاس دیگری که در فایل IdentityConfig.cs وجود دارند کلاس‌های EmailService و SmsService هستند. بصورت پیش فرض این کلاس‌ها تنها یک wrapper هستند که می‌توانید با توسعه آنها سرویس‌های مورد نیاز برای احراز هویت دو مرحله ای و تایید حساب‌های کاربری را بسازید.
public class EmailService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Plug in your email service here to send an email.
        return Task.FromResult(0);
    }
}

public class SmsService : IIdentityMessageService
{
    public Task SendAsync(IdentityMessage message)
    {
        // Plug in your sms service here to send a text message.
        return Task.FromResult(0);
    }
}
دقت کنید که هر دو این کلاس‌ها قرارداد واحدی را بنام IIdentityMessageService پیاده سازی می‌کنند. همچنین قطعه کد زیر را از متد ()ApplicationUserManager.Create بیاد آورید.
// Register two factor authentication providers. This application uses 
// Phone and Emails as a step of receiving a code for verifying 
// the user You can write your own provider and plug in here.
manager.RegisterTwoFactorProvider("PhoneCode", 
    new PhoneNumberTokenProvider<ApplicationUser>
{
    MessageFormat = "Your security code is: {0}"
});

manager.RegisterTwoFactorProvider("EmailCode", 
    new EmailTokenProvider<ApplicationUser>
{
    Subject = "SecurityCode",
    BodyFormat = "Your security code is {0}"
});

manager.EmailService = new EmailService();
manager.SmsService = new SmsService();
همانطور که می‌بینید در متد ()Create کلاس‌های EmailService و SmsService وهله سازی شده و توسط خواص مرتبط به وهله ApplicationUserManager ارجاع می‌شوند.


کلاس کمکی SignIn
هنگام توسعه پروژه مثال Identity، تیم توسعه دهندگان کلاسی کمکی برای ما ساخته‌اند که فرامین عمومی احراز هویت کاربران و ورود آنها به اپلیکیشن را توسط یک API ساده فراهم می‌سازد. برای آشنایی با نحوه استفاده از این متد‌ها می‌توانیم به کنترلر AccountController در پوشه Controllers مراجعه کنیم. اما پیش از آن بگذارید نگاهی به خود کلاس SignInHelper داشته باشیم.
public class SignInHelper
{
    public SignInHelper(
        ApplicationUserManager userManager, 
        IAuthenticationManager authManager)
    {
        UserManager = userManager;
        AuthenticationManager = authManager;
    }

    public ApplicationUserManager UserManager { get; private set; }
    public IAuthenticationManager AuthenticationManager { get; private set; }

    public async Task SignInAsync(
        ApplicationUser user, 
        bool isPersistent, 
        bool rememberBrowser)
    {
        // Clear any partial cookies from external or two factor partial sign ins
        AuthenticationManager.SignOut(
            DefaultAuthenticationTypes.ExternalCookie, 
            DefaultAuthenticationTypes.TwoFactorCookie);

        var userIdentity = await user.GenerateUserIdentityAsync(UserManager);

        if (rememberBrowser)
        {
            var rememberBrowserIdentity = 
                AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(user.Id);

            AuthenticationManager.SignIn(
                new AuthenticationProperties { IsPersistent = isPersistent }, 
                userIdentity, 
                rememberBrowserIdentity);
        }
        else
        {
            AuthenticationManager.SignIn(
                new AuthenticationProperties { IsPersistent = isPersistent }, 
                userIdentity);
        }
    }

    public async Task<bool> SendTwoFactorCode(string provider)
    {
        var userId = await GetVerifiedUserIdAsync();

        if (userId == null)
        {
            return false;
        }

        var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider);

        // See IdentityConfig.cs to plug in Email/SMS services to actually send the code
        await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token);

        return true;
    }

    public async Task<string> GetVerifiedUserIdAsync()
    {
        var result = await AuthenticationManager.AuthenticateAsync(
            DefaultAuthenticationTypes.TwoFactorCookie);

        if (result != null && result.Identity != null 
            && !String.IsNullOrEmpty(result.Identity.GetUserId()))
        {
            return result.Identity.GetUserId();
        }

        return null;
    }

    public async Task<bool> HasBeenVerified()
    {
        return await GetVerifiedUserIdAsync() != null;
    }

    public async Task<SignInStatus> TwoFactorSignIn(
        string provider, 
        string code, 
        bool isPersistent, 
        bool rememberBrowser)
    {
        var userId = await GetVerifiedUserIdAsync();

        if (userId == null)
        {
            return SignInStatus.Failure;
        }

        var user = await UserManager.FindByIdAsync(userId);

        if (user == null)
        {
            return SignInStatus.Failure;
        }

        if (await UserManager.IsLockedOutAsync(user.Id))
        {
            return SignInStatus.LockedOut;
        }

        if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code))
        {
            // When token is verified correctly, clear the access failed 
            // count used for lockout
            await UserManager.ResetAccessFailedCountAsync(user.Id);
            await SignInAsync(user, isPersistent, rememberBrowser);

            return SignInStatus.Success;
        }

        // If the token is incorrect, record the failure which 
        // also may cause the user to be locked out
        await UserManager.AccessFailedAsync(user.Id);

        return SignInStatus.Failure;
    }

    public async Task<SignInStatus> ExternalSignIn(
        ExternalLoginInfo loginInfo, 
        bool isPersistent)
    {
        var user = await UserManager.FindAsync(loginInfo.Login);

        if (user == null)
        {
            return SignInStatus.Failure;
        }

        if (await UserManager.IsLockedOutAsync(user.Id))
        {
            return SignInStatus.LockedOut;
        }

        return await SignInOrTwoFactor(user, isPersistent);
    }

    private async Task<SignInStatus> SignInOrTwoFactor(
        ApplicationUser user, 
        bool isPersistent)
    {
        if (await UserManager.GetTwoFactorEnabledAsync(user.Id) &&
            !await AuthenticationManager.TwoFactorBrowserRememberedAsync(user.Id))
        {
            var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));

            AuthenticationManager.SignIn(identity);
            return SignInStatus.RequiresTwoFactorAuthentication;
        }

        await SignInAsync(user, isPersistent, false);
        return SignInStatus.Success;
    }

    public async Task<SignInStatus> PasswordSignIn(
        string userName, 
        string password, 
        bool isPersistent, 
        bool shouldLockout)
    {
        var user = await UserManager.FindByNameAsync(userName);

        if (user == null)
        {
            return SignInStatus.Failure;
        }

        if (await UserManager.IsLockedOutAsync(user.Id))
        {
            return SignInStatus.LockedOut;
        }

        if (await UserManager.CheckPasswordAsync(user, password))
        {
            return await SignInOrTwoFactor(user, isPersistent);
        }

        if (shouldLockout)
        {
            // If lockout is requested, increment access failed 
            // count which might lock out the user
            await UserManager.AccessFailedAsync(user.Id);

            if (await UserManager.IsLockedOutAsync(user.Id))
            {
                return SignInStatus.LockedOut;
            }
        }

        return SignInStatus.Failure;
    }
}
کد این کلاس نسبتا طولانی است، و بررسی عمیق آنها از حوصله این مقاله خارج است. گرچه اگر به دقت یکبار این کلاس را مطالعه کنید می‌توانید براحتی از نحوه کارکرد آن آگاه شوید. همانطور که می‌بینید اکثر متدهای این کلاس مربوط به ورود کاربران و مسئولیت‌های تعیین سطوح دسترسی است.

این متدها ویژگی‌های جدیدی که در Identity 2.0 عرضه شده اند را در بر می‌گیرند. متد آشنایی بنام ()SignInAsync را می‌بینیم، و متدهای دیگری که مربوط به احراز هویت دو مرحله ای و external log-ins می‌شوند. اگر به متدها دقت کنید خواهید دید که برای ورود کاربران به اپلیکیشن کارهای بیشتری نسبت به نسخه پیشین انجام می‌شود.

بعنوان مثال متد Login در کنترلر AccountController را باز کنید تا نحوه مدیریت احراز هویت در Identity 2.0 را ببینید.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    // This doen't count login failures towards lockout only two factor authentication
    // To enable password failures to trigger lockout, change to shouldLockout: true
    var result = await SignInHelper.PasswordSignIn(
        model.Email, 
        model.Password, 
        model.RememberMe, 
        shouldLockout: false);

    switch (result)
    {
        case SignInStatus.Success:
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresTwoFactorAuthentication:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");

            return View(model);
    }
}

مقایسه Sign-in با نسخه Identity 1.0
در نسخه 1.0 این فریم ورک، ورود کاربران به اپلیکیشن مانند لیست زیر انجام می‌شد. اگر متد Login در کنترلر AccountController را باز کنید چنین قطعه کدی را می‌بینید.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        var user = await UserManager.FindAsync(model.UserName, model.Password);

        if (user != null)
        {
            await SignInAsync(user, model.RememberMe);
            return RedirectToLocal(returnUrl);
        }
        else
        {
            ModelState.AddModelError("", "Invalid username or password.");
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}
در قطعه کد بالا متدی در کلاس UserManager را فراخوانی می‌کنیم که مشابه قطعه کدی است که در کلاس SignInHelper دیدیم. همچنین متد SignInAsync را فراخوانی می‌کنیم که مستقیما روی کنترلر AccountController تعریف شده است.
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(
        DefaultAuthenticationTypes.ExternalCookie);

    var identity = await UserManager.CreateIdentityAsync(
        user, DefaultAuthenticationTypes.ApplicationCookie);

    AuthenticationManager.SignIn(
        new AuthenticationProperties() { IsPersistent = isPersistent }, identity);
}
مسلما با عرضه قابلیت‌های جدید در Identity 2.0 و تغییرات معماری این فریم ورک، پیچیدگی هایی معرفی می‌شود که حتی در امور ساده ای مانند ورود کاربران نیز قابل مشاهده است.


ApplicationDbContext
اگر از نسخه پیشین Identity در اپلیکیشن‌های ASP.NET MVC استفاده کرده باشید با کلاس ApplicationDbContext آشنا هستید. این کلاس پیاده سازی پیش فرض EF فریم ورک است، که اپلیکیشن شما توسط آن داده‌های مربوط به Identity را ذخیره و بازیابی می‌کند.

در پروژه مثال ها، تیم Identity این کلاس را بطور متفاوتی نسبت به نسخه 1.0 پیکربندی کرده اند. اگر فایل IdentityModels.cs را باز کنید تعاریف کلاس ApplicationDbContext را مانند لیست زیر خواهید یافت.
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> {
    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false) {
    }

    static ApplicationDbContext() {
        // Set the database intializer which is run once during application start
        // This seeds the database with admin user credentials and admin role
        Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
    }

    public static ApplicationDbContext Create() {
        return new ApplicationDbContext();
    }
}
قطعه کد بالا دو متد استاتیک تعریف می‌کند. یکی ()Create و دیگری ()ApplicationDbContext که سازنده دیتابیس (database initializer) را تنظیم می‌کند. این متد هنگام اجرای اپلیکیشن فراخوانی می‌شود و هر پیکربندی ای که در کلاس ApplicationDbInitializer تعریف شده باشد را اجرا می‌کند. اگر به فایل IdentityConfig.cs مراجعه کنیم می‌توانیم تعاریف این کلاس را مانند لیست زیر بیابیم.
public class ApplicationDbInitializer 
    : DropCreateDatabaseIfModelChanges<ApplicationDbContext> 
{
    protected override void Seed(ApplicationDbContext context) 
    {
        InitializeIdentityForEF(context);
        base.Seed(context);
    }

    public static void InitializeIdentityForEF(ApplicationDbContext db) 
    {
        var userManager = HttpContext
            .Current.GetOwinContext()
            .GetUserManager<ApplicationUserManager>();

        var roleManager = HttpContext.Current
            .GetOwinContext()
            .Get<ApplicationRoleManager>();

        const string name = "admin@admin.com";
        const string password = "Admin@123456";
        const string roleName = "Admin";

        //Create Role Admin if it does not exist
        var role = roleManager.FindByName(roleName);

        if (role == null) 
        {
            role = new IdentityRole(roleName);
            var roleresult = roleManager.Create(role);
        }

        var user = userManager.FindByName(name);

        if (user == null) 
        {
            user = new ApplicationUser { UserName = name, Email = name };

            var result = userManager.Create(user, password);
            result = userManager.SetLockoutEnabled(user.Id, false);
        }

        // Add user admin to Role Admin if not already added
        var rolesForUser = userManager.GetRoles(user.Id);

        if (!rolesForUser.Contains(role.Name)) 
        {
            var result = userManager.AddToRole(user.Id, role.Name);
        }
    }
}
پیکربندی جاری در صورتی که مدل موجودیت‌ها تغییر کنند دیتابیس را پاک کرده و مجددا ایجاد می‌کند. در غیر اینصورت از دیتابیس موجود استفاده خواهد شد. اگر بخواهیم با هر بار اجرای اپلیکیشن دیتابیس از نو ساخته شود، می‌توانیم کلاس مربوطه را به <DropCreateDatabaseAlways<ApplicationDbContext تغییر دهیم. بعنوان مثال هنگام توسعه اپلیکیشن و بمنظور تست می‌توانیم از این رویکرد استفاده کنیم تا هر بار با دیتابیسی (تقریبا) خالی شروع کنیم.

نکته حائز اهمیت دیگر متد ()InitializeIdentityForEF است. این متد کاری مشابه متد ()Seed انجام می‌دهد که هنگام استفاده از مهاجرت‌ها (Migrations) از آن استفاده می‌کنیم. در این متد می‌توانید رکوردهای اولیه ای را در دیتابیس ثبت کنید. همانطور که مشاهده می‌کنید در قطعه کد بالا نقشی مدیریتی بنام Admin ایجاد شده و کاربر جدیدی با اطلاعاتی پیش فرض ساخته می‌شود که در آخر به این نقش منتسب می‌گردد. با انجام این مراحل، پس از اجرای اولیه اپلیکیشن کاربری با سطح دسترسی مدیر در اختیار خواهیم داشت که برای تست اپلیکیشن بسیار مفید خواهد بود.

در این مقاله نگاهی اجمالی به Identity 2.0 در پروژه‌های ASP.NET MVC داشتیم. کامپوننت‌های مختلف فریم ورک و نحوه پیکربندی آنها را بررسی کردیم و با تغییرات و قابلیت‌های جدید به اختصار آشنا شدیم. در مقالات بعدی بررسی هایی عمیق‌تر خواهیم داشت و با نحوه استفاده و پیاده سازی قسمت‌های مختلف این فریم ورک آشنا خواهیم شد.


مطالعه بیشتر