اشتراکها
اشتراکها
وضعیت دورکاری در سال 2020
امروزه در بسیاری از محیطهای برنامه نویسی جاوا و اندروید، استفاده از این سیستم رایج است. ولی هنوز دیده میشود عدهای نسبت به آن دید روشنی ندارند و برای آنها ناشناخته است و در حد یک سیستم کانفیگ آن را میشناسند. در این مقاله قصد داریم که مفهوم روشنتری از این سیستم را داشته باشم و بدانیم هدف آن چیست و چگونه کار میکند تا از این به بعد دیگر آن را به چشم یک کانفیگ کنندهی ساده نگاه نکنیم. قبل از هر چیزی بهتر است که با تعدادی از اصطلاحات آن آشنا شویم تا در متن مقاله به مشکل برنخوریم:
DSL یا Domain Specific languages به معنی زبانهایی با دامنه محدود است که برای اهداف خاصی نوشته میشوند و تنها بر روی یک جنبه از هدف تمرکز دارند. این زبانها به شما اجازه نمیدهند که یک برنامه را به طور کامل با آن بنویسید. بلکه به شما اجازه میدهند به هدفی که برای آن نوشته شدهاند، برسید. یکی از این زبانها همان css هست که با آن کار میکنید. این زبان به صورت محدود تنها بر روی یک جنبه و آن، تزئین سازی المانهای وب، تمرکز دارد. در وقع مثل زبان سی شارپ همه منظوره نیست و محدودهای مشخص برای خود دارد. به این نوع از زبانهای DSL، نوع اکسترنال هم میگویند. چون زبانی مستقل برای خود است و به زبان دیگری وابستگی ندارد. ولی در یک زبان اینترنال، وابستگی به زبان دیگری وجود دارد. مثل Fluent Interfaceها که به ما شیوه آسانی از دسترسی به جنبههای یک شیء را میدهد. برای آشنایی هر چه بیشتر با این زبانها و ساختار آن، کتاب Domain Specific languages نوشته آقای مارتین فاولر توصیه میشود.
Groovy یک زبان شیء گرای DSL هست که برای پلتفرم جاوا ساخته شده است. برای اطلاعات بیشتر در مورد این زبان، صفحه ویکی ، میتواند مفید واقع شود.
از دیرباز سیستمهای Ant و Maven وجود داشتند و کار آنها اتوماسیون بعضی اعمال بود. ولی بعد از مدتی سیستم Gradle یا جمع کردن نقاط قوت آنها و افزودن ویژگیهای قدرتمندتری به خود، پا به میدان گذاشت تا راحتی بیشتری را برای برنامه نویس فراهم کند. از ویژگیهای گریدل میتوان داشتن زبان گرووی اشاره کرده که قدرت بیشتری را نسبت به سایر سیستمها داشت و مزیت مهم دیگر این بود که انعطاف بالایی را جهت افزودن پلاگینها داشت و گوگل با استفاده از این قابلیت، پشتیبانی از گریدل را در اندروید استادیو نیز گنجاند تا راحتی بیشتری را در اتوماسیون وظایف سیستمی ایجاد کند. در واقع آنچه شما در سیستم گریدل کار میکنید و اطلاعات خود را با آن کانفیگ میکنید، پلاگینی است که از سمت گوگل در اختیار شما قرار گرفته است و در مواقع خاص این وظایف توسط پلاگینها اجرا میشوند.
گریدل به راحتی از سایت رسمی آن قابل دریافت است و میتوان آن را در پروژههای جاوایی که مدنظر شماست، دریافت کنید و با استفاده از خط فرمان، با آن تعامل کنید. هر چند امروزه اکثر ویراستارهای جاوا از آن پشتیبانی میکنند.
گریدل یک ماهیت توصیفی دارد که شما تنها لازم است اعمالی را برای آن توصیف کنید تا بقیه کارها را انجام دهد. گریدل در پشت صحنه از یک "گراف جهت دار بدون دور" Directed Acycllic Graph یا به اختصار DAG استفاده میکند و طبق آن ترتیب وظایف یا taskها را دانسته و آنها را اجرا میکند. گریدل با این DAG، سه فاز آماده سازی، پیکربندی و اجرا را انجام میدهد.
در این فایل سه پروژه برای گریدل مشخص شدهاند. البته از نگاه Intellij سه ماژول معرفی شدهاند و این فایل برای یک پروژه اختیاری است. گریدل برای پیدا کردن این فایل، از الگوریتمهای متفاوتی استفاده میکند.
الان پروژه dbutiilities در سطح بالاتری از دایرکتوری جاری قرار گرفته است.
در کلوژر buildscript مخازنی را که کتابخانههای نامبرده (وابستگی ها) در این کلوژ قرار میگیرند، معرفی میکنیم. در کلوژر بعدی تنظمیاتی را که برای همه پروژهها اعمال میشوند، انجام میدهیم که در آن به معرفی مخزن jcenter پرداختیم. کتابخانهای که در کلوژر buildscript صدا زدیم، همان پلاگینی است که گوگل برای گریدل منتشر کرده که ما به آن ابزار بیلد میگوییم. حال به سراغ دیگر فایلهای منحصر به فرد هر پروژه بروید. در این فایلها، شما تنظیمات پیشفرضی را میبینید که یکی از آنها نسخه بندی پروژه است. پروژههای وابستهای را که از مخازن باید دریافت شوند، شامل میشود و بسیاری از گزینههای دیگری که برای شما مهیا شده است تا در فاز پیکربندی، وظایف را بسازید.
در مرحله اجرا هم این وظایف را اجرا میکنیم. تمامی این سه عملیات توسط فایل و دستوری به نام gradlew که برگرفته از gradleWrapper میباشد انجام میشود. اگر در ترمینال اندروید استادیو این عبارت را تایپ کنید، میتوانید در ادامه دستور پیامهای مربوط به این عملیات را ببینید و ترتیب اجرای فازها را مشاهده کنید.
بیایید یک task را تعریف کنیم
در ابتدا عبارت task را به عنوان معرفی task میآوریم و سپس نام آن را وارد میکنیم. بعد از آن ما از عبارتهای شیفت چپ>> استفاده کردیم. این عبارت شیفت چپ به این معناست که تسک مربوطه را آخر از همه وظایف اجرا کن که این عمل از طریق اجرای متدی به نام doLast صورت میگیرد. اگر در ترمینال اندروید عبارت زیر را تایپ کنید، متن مورد نظر باید نمایش یابد:
برای نمایش اطلاعات بیشتر میتوانید از فلگ info هم استفاده کنید:
حال شاید بگویید من در بعضی از سایتها یا مستندات و یا پروژههای دیگر دیدهام که عبارت >> را قرار نمیدهند. در این مورد باید گفت که آنها در واقع تسکهای اجرایی نیستند و برای پیکربندی به کار میروند و در فاز پیکربندی هم اجرا میشوند که در ادامه نمونه آن را خواهیم دید.
اگر بخواهید خودتان دستی یک تسک پیکربندی را به یک تسک اجرایی تبدیل کنید، میتوانید متد doLast را صدا بزنید. کد زیر را توسط gradlew اجرا کنید؛ به همراه اطلاعات verbose تا ببینید که هر کدام از پیامها در کدام بخش چاپ میشوند. پیام اول در فاز پیکربندی و پیام دوم در فاز اجرایی چاپ میشوند.
یکی از کارهایی که در یک تسک میتوانید انجام دهید این است که آن را به یک تسک دیگر وابسته کنید. به عنوان مثال ما قصد داریم بعد از تسک mytask1، تسک my task2 اجرا شود و زمان پایان تسک mytask1 را در خروجی نمایش دهیم. برای اینکار باید بین تسکها یک وابستگی ایجاد شود و سپس با متد doLast کد خودمان را اجرایی نماییم. البته توجه داشته باشید که این وابستگیها تنها به تسکهای داخل فایل گریدل انجام میشود و نه تسکهای پلاگینها یا وابستگی هایی که تعریف میکنیم.
سپس تسک شماره دو را صدا میزنیم. در اینجا جون تسک شماره دو به تسک شماره یک وابسته است، ابتدا تسک شماره یک اجرا شده و سپس نوبت تسک شماره دو میشود.
خروجی کار:
در گریدل مخالف doLast یعنی doFirst را نیز داریم ولی عملگر جایگزینی برای آن وجود ندارد و مستقیما باید آن را پیاده سازی کنید. خود گریدل به طور پیش فرض نیز تسکهای آماده ای نیز دارد که میتوانید در مستندات آن بیابید. به عنوان مثال یکی از تسکهای مفید و کاربردی آن تسک کپی کردن هست که از طریق آن میتوانید فایلی یا فایلهایی را از یک مسیر به مسیر دیگر کپی کنید. برای استفاده از چنین تسکهایی، باید تسکهای خود را به شکل زیر به شیوه اکشن بنویسید:
در تسک بالا بعد از اجرای تسک شماره یک، آخرین کاری که انجام میشود این است که فایلهای apk موجود در زیر دایرکتوریهای مسیر from به ریشه اصلی کپی خواهند شد. پس همانطور که میبینید گریدل به راحتی عملیات اتوماسیون را انجام میدهد.
برای نمایش تسکهای موجود میتوانید از گریدل درخواست کنید که لیست تمامی تسکهای موجود را به شما نشان دهد. برای اینکار میتوانید دستور زیر را صدا کنید:
بعد از مدتی تمامی تسکهای موجود به صورت گروه بندی نمایش داده شده و تسکهایی که شما جدیدا اضافه کردید را با عنوان other tasks نمایش خواهد داد:
اگر به تسکهای خود گریدل نگاه کنید برای هر کدام توضیحی هم وجود دارد؛ اگر شما هم قصد دارید توضیحی اضافه کنید از خصوصیت description استفاده کنید:
یکبار دیگر دستور نمایش تسکها را صدا بزنید تا اینبار توضیح اضافه شده نمایش داده شود.
یکی دیگر از نکات جالب در مورد گریدل این است که میتواند برای شما callback ارسال کند. بدین صورت که اگر اتفاقی خاصی افتاد، تسک خاصی را اجرا کند. به عنوان مثال ما در کد پایین تسکی را ایجاد کردهایم که به ما این اجازه را میدهد، هر موقع تسکی در مرحله پیکربندی به بیلد اضافه میشود، تسک ما هم اجرا شود و نام تسک اضافه شده به بیلد را چاپ میکند.
گریدل امکانات دیگری چون بررسی استثناءها و ایجاد استثناءها را هم پوشش میدهد که میتوانید در این صفحه آن را پیگیری کنید.
Gradle Wrapper
گریدل در حال حاضر مرتبا در حال تغییر و به روز رسانی است و اگر بخواهیم مستقیما با گریدل کار کنیم ممکن است که به مشکلاتی که در نسخه بندی است برخورد کنیم. از آنجا که هر پروژهای که روی سیستم شما قرار بگیرد از نسخهای متفاوتی از گریدل استفاده کند، باعث میشود که نتوانید نسخه مناسبی از گریدل را برای سیستم خود دانلود کنید. بدین جهت wrapper ایجاد شد تا دیگر نیازی به نصب گریدل پیدا نکنید. wrapper در هر پروژه میداند که که به چه نسخهای از گریدل نیاز است. پس موقعی که شما دستور gradlew را صدا میزنید در ویندوز فایل gradlew.bat صدا زده شده و یا در لینوکس و مک فایل شِل اسکریپت gradlew صدا زده میشود و wrapper به خوبی میداند که به چه نسخهای از گریدل برای اجرا نیاز دارد و آن را از طریق دانلود فراهم میکند. اگر همینک دایرکتوری والد پروژه اندرویدی خود را نگاه کنید میتوانید این دو فایل را ببینید.
از آنجا که خود اندروید استادیو به ساخت wrapper اقدام میکند، شما راحت هستید. ولی اگر دوست دارید خودتان برای پروژهای wrapper تولید کنید، مراحل زیر را دنبال کنید:
برای ایجاد wrapper توسط خودتان باید گریدل را دانلود و روی سیستم نصب کنید و سپس دستور زیر را صادر کنید:
دستور بالا یک wrapper برای نسخه 2.4 ایجاد میکند.
اگر میخواهید ببینید wrapper که اندروید استادیو شما دارد چه نسخه از گردیل را صدا میزند مسیر را از دایرکتوری پروژه دنبال کنید و فایل زیر را بگشایید:
هنگامی که گوگل قصد آپدیت نسخه گریدل شما را بکند این فایل را باز کرده و نسخه داخل آن را ویرایش میکند.
اینها فقط مختصراتی از آشنایی با نحوه عملکر گریدل برای داشتن دیدی روشنتر نسبت به آن بود. برای آشنایی بیشتر با گریدل، باید مستندات رسمی آن را دنبال کنید.
DSL یا Domain Specific languages به معنی زبانهایی با دامنه محدود است که برای اهداف خاصی نوشته میشوند و تنها بر روی یک جنبه از هدف تمرکز دارند. این زبانها به شما اجازه نمیدهند که یک برنامه را به طور کامل با آن بنویسید. بلکه به شما اجازه میدهند به هدفی که برای آن نوشته شدهاند، برسید. یکی از این زبانها همان css هست که با آن کار میکنید. این زبان به صورت محدود تنها بر روی یک جنبه و آن، تزئین سازی المانهای وب، تمرکز دارد. در وقع مثل زبان سی شارپ همه منظوره نیست و محدودهای مشخص برای خود دارد. به این نوع از زبانهای DSL، نوع اکسترنال هم میگویند. چون زبانی مستقل برای خود است و به زبان دیگری وابستگی ندارد. ولی در یک زبان اینترنال، وابستگی به زبان دیگری وجود دارد. مثل Fluent Interfaceها که به ما شیوه آسانی از دسترسی به جنبههای یک شیء را میدهد. برای آشنایی هر چه بیشتر با این زبانها و ساختار آن، کتاب Domain Specific languages نوشته آقای مارتین فاولر توصیه میشود.
Groovy یک زبان شیء گرای DSL هست که برای پلتفرم جاوا ساخته شده است. برای اطلاعات بیشتر در مورد این زبان، صفحه ویکی ، میتواند مفید واقع شود.
از دیرباز سیستمهای Ant و Maven وجود داشتند و کار آنها اتوماسیون بعضی اعمال بود. ولی بعد از مدتی سیستم Gradle یا جمع کردن نقاط قوت آنها و افزودن ویژگیهای قدرتمندتری به خود، پا به میدان گذاشت تا راحتی بیشتری را برای برنامه نویس فراهم کند. از ویژگیهای گریدل میتوان داشتن زبان گرووی اشاره کرده که قدرت بیشتری را نسبت به سایر سیستمها داشت و مزیت مهم دیگر این بود که انعطاف بالایی را جهت افزودن پلاگینها داشت و گوگل با استفاده از این قابلیت، پشتیبانی از گریدل را در اندروید استادیو نیز گنجاند تا راحتی بیشتری را در اتوماسیون وظایف سیستمی ایجاد کند. در واقع آنچه شما در سیستم گریدل کار میکنید و اطلاعات خود را با آن کانفیگ میکنید، پلاگینی است که از سمت گوگل در اختیار شما قرار گرفته است و در مواقع خاص این وظایف توسط پلاگینها اجرا میشوند.
گریدل به راحتی از سایت رسمی آن قابل دریافت است و میتوان آن را در پروژههای جاوایی که مدنظر شماست، دریافت کنید و با استفاده از خط فرمان، با آن تعامل کنید. هر چند امروزه اکثر ویراستارهای جاوا از آن پشتیبانی میکنند.
گریدل یک ماهیت توصیفی دارد که شما تنها لازم است اعمالی را برای آن توصیف کنید تا بقیه کارها را انجام دهد. گریدل در پشت صحنه از یک "گراف جهت دار بدون دور" Directed Acycllic Graph یا به اختصار DAG استفاده میکند و طبق آن ترتیب وظایف یا taskها را دانسته و آنها را اجرا میکند. گریدل با این DAG، سه فاز آماده سازی، پیکربندی و اجرا را انجام میدهد.
- در مرحله آماده سازی ما به گریدل میگوییم چه پروژه یا پروژههایی نیاز به بیلد شدن دارند. در اندروید استادیو، این مرحله در فایل settings.gradle انجام میشود؛ شما در این فایل مشخص میکنید چه پروژههای نیاز به بیلد شدن توسط گریدل دارند. ساختار این فایل به این شکل است:
include ':ActiveAndroid-master', ':app', ':dbutilities'
-
در اولین مرحله انتظار دارد که فایل settings در دایرکتوری جاری باشد و اگر آن را پیدا کرد آن را مورد استفاده قرار میدهد؛ در غیر اینصورت مرحله بعدی را آغاز میکند.
- در مرحله دوم، در این دایرکتوری به دنبال دایرکتوری به نام master میگردد و اگر در آن هم یافت نکرد مرحله سوم را آغاز میکند.
- در مرحله سوم، جست و جو در دایرکتوری والد انجام میشود
- چنانچه این فایل را در هیچ یک از احتمالات بالا نیابد، همین پروژه جاری را تشخیص خواهد داد.
include ':ActiveAndroid-master', ':app', ':dbutilities' project('dbutilities').projectDir=new File(settingsDir,'../dir1/dir2');
-
در مرحله پیکربندی، وظایف یا taskها را معرفی میکنیم. این عمل پیکربندی توسط فایل build.gradle که برای پروژه اصلی و هر زیر پروژهای که مشخص شدهاند، صورت میگیرد. در این فایل شما میتوانید خواص و متدهایی را تعریف و و ظایفی را مشخص کنید.
در پروژه اصلی، فایل BuildGradle شامل خطوط زیر است:
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.5.0' } } allprojects { repositories { jcenter() } }
در مرحله اجرا هم این وظایف را اجرا میکنیم. تمامی این سه عملیات توسط فایل و دستوری به نام gradlew که برگرفته از gradleWrapper میباشد انجام میشود. اگر در ترمینال اندروید استادیو این عبارت را تایپ کنید، میتوانید در ادامه دستور پیامهای مربوط به این عملیات را ببینید و ترتیب اجرای فازها را مشاهده کنید.
بیایید یک task را تعریف کنیم
task mytask <<{ println ".net tips task in config phase" }
gradlew mytask
gradlew --info mytask
اگر بخواهید خودتان دستی یک تسک پیکربندی را به یک تسک اجرایی تبدیل کنید، میتوانید متد doLast را صدا بزنید. کد زیر را توسط gradlew اجرا کنید؛ به همراه اطلاعات verbose تا ببینید که هر کدام از پیامها در کدام بخش چاپ میشوند. پیام اول در فاز پیکربندی و پیام دوم در فاز اجرایی چاپ میشوند.
task mytask { println ".net tips task in config phase" doLast{ println ".net tips task in exe phase" } }
یکی از کارهایی که در یک تسک میتوانید انجام دهید این است که آن را به یک تسک دیگر وابسته کنید. به عنوان مثال ما قصد داریم بعد از تسک mytask1، تسک my task2 اجرا شود و زمان پایان تسک mytask1 را در خروجی نمایش دهیم. برای اینکار باید بین تسکها یک وابستگی ایجاد شود و سپس با متد doLast کد خودمان را اجرایی نماییم. البته توجه داشته باشید که این وابستگیها تنها به تسکهای داخل فایل گریدل انجام میشود و نه تسکهای پلاگینها یا وابستگی هایی که تعریف میکنیم.
task mytask1 << { println ".net tips is the best" } task mytask2() { dependsOn mytask1 doLast{ Date time=Calendar.getInstance().getTime(); SimpleDateFormat formatter=new SimpleDateFormat("HH:mm:ss , YYYY/MM/dd"); println "mytask1 is done at " + formatter.format(time); } }
gradlew --info mytask2
Executing task ':app:mytask1' (up-to-date check took 0.003 secs) due to: Task has not declared any outputs. خروجی تسک شماره یک .net tips is the best :app:mytask1 (Thread[main,5,main]) completed. Took 0.046 secs. :app:mytask2 (Thread[main,5,main]) started. :app:mytask2 Executing task ':app:mytask2' (up-to-date check took 0.0 secs) due to: Task has not declared any outputs. خروجی تسک شماره دو mytask1 is done at 04:03:09 , 2016/07/07 :app:mytask2 (Thread[main,5,main]) completed. Took 0.075 secs. BUILD SUCCESSFUL
در گریدل مخالف doLast یعنی doFirst را نیز داریم ولی عملگر جایگزینی برای آن وجود ندارد و مستقیما باید آن را پیاده سازی کنید. خود گریدل به طور پیش فرض نیز تسکهای آماده ای نیز دارد که میتوانید در مستندات آن بیابید. به عنوان مثال یکی از تسکهای مفید و کاربردی آن تسک کپی کردن هست که از طریق آن میتوانید فایلی یا فایلهایی را از یک مسیر به مسیر دیگر کپی کنید. برای استفاده از چنین تسکهایی، باید تسکهای خود را به شکل زیر به شیوه اکشن بنویسید:
task mytask(type:Copy) { dependsOn mytask1 doLast{ from('build/apk') { include '**/*.apk' } into '.' } }
برای نمایش تسکهای موجود میتوانید از گریدل درخواست کنید که لیست تمامی تسکهای موجود را به شما نشان دهد. برای اینکار میتوانید دستور زیر را صدا کنید:
gradlew --info tasks
Other tasks ----------- clean jarDebugClasses jarReleaseClasses mytask mytask2 transformResourcesWithMergeJavaResForDebugUnitTest transformResourcesWithMergeJavaResForReleaseUnitTest
task mytask(type:Copy) { description "copy apk files to root directory" dependsOn mytask1 doLast{ from('build/apk') { include '**/*.apk' } into '.' } }
یکی دیگر از نکات جالب در مورد گریدل این است که میتواند برای شما callback ارسال کند. بدین صورت که اگر اتفاقی خاصی افتاد، تسک خاصی را اجرا کند. به عنوان مثال ما در کد پایین تسکی را ایجاد کردهایم که به ما این اجازه را میدهد، هر موقع تسکی در مرحله پیکربندی به بیلد اضافه میشود، تسک ما هم اجرا شود و نام تسک اضافه شده به بیلد را چاپ میکند.
tasks.whenTaskAdded{ task-> println "task is added $task.name" }
گریدل امکانات دیگری چون بررسی استثناءها و ایجاد استثناءها را هم پوشش میدهد که میتوانید در این صفحه آن را پیگیری کنید.
Gradle Wrapper
گریدل در حال حاضر مرتبا در حال تغییر و به روز رسانی است و اگر بخواهیم مستقیما با گریدل کار کنیم ممکن است که به مشکلاتی که در نسخه بندی است برخورد کنیم. از آنجا که هر پروژهای که روی سیستم شما قرار بگیرد از نسخهای متفاوتی از گریدل استفاده کند، باعث میشود که نتوانید نسخه مناسبی از گریدل را برای سیستم خود دانلود کنید. بدین جهت wrapper ایجاد شد تا دیگر نیازی به نصب گریدل پیدا نکنید. wrapper در هر پروژه میداند که که به چه نسخهای از گریدل نیاز است. پس موقعی که شما دستور gradlew را صدا میزنید در ویندوز فایل gradlew.bat صدا زده شده و یا در لینوکس و مک فایل شِل اسکریپت gradlew صدا زده میشود و wrapper به خوبی میداند که به چه نسخهای از گریدل برای اجرا نیاز دارد و آن را از طریق دانلود فراهم میکند. اگر همینک دایرکتوری والد پروژه اندرویدی خود را نگاه کنید میتوانید این دو فایل را ببینید.
از آنجا که خود اندروید استادیو به ساخت wrapper اقدام میکند، شما راحت هستید. ولی اگر دوست دارید خودتان برای پروژهای wrapper تولید کنید، مراحل زیر را دنبال کنید:
برای ایجاد wrapper توسط خودتان باید گریدل را دانلود و روی سیستم نصب کنید و سپس دستور زیر را صادر کنید:
gradle wrapper --gradle-version 2.4
اگر میخواهید ببینید wrapper که اندروید استادیو شما دارد چه نسخه از گردیل را صدا میزند مسیر را از دایرکتوری پروژه دنبال کنید و فایل زیر را بگشایید:
\gradle\wrapper\gradle-wrapper.properties
اینها فقط مختصراتی از آشنایی با نحوه عملکر گریدل برای داشتن دیدی روشنتر نسبت به آن بود. برای آشنایی بیشتر با گریدل، باید مستندات رسمی آن را دنبال کنید.
سال قبل نتیجهی جستجوی من برای یافتن فرمول محاسبهی زمان سال تحویل، برای ارسال ایمیلهای خودکار تبریک آن، در سایتهای ایرانی حاصلی نداشت. اما واژهی انگلیسی Equinox سرآغازی شد برای یافتن این الگوریتم.
نام علمی لحظهی سال تحویل، Vernal Equinox است. Equinox به معنای نقطهای است که یک فصل، به فصلی دیگر تبدیل میشود:
Equinox واژهای است لاتین به معنای «شبهای مساوی» و به این نکته اشاره دارد که در Equinox، طول شب و روز یکی میشوند. هر سال دارای دو Equinox است: vernal equinox و autumnal equinox (بهاری و پائیزی). البته باید درنظر داشت که Equinox بهاری در نیم کرهی شمالی بیشتر معنا پیدا میکند؛ زیرا در نیم کرهی جنوبی در همین زمان، پائیز شروع میشود.
بنابراین میتوان enum زیر را برای تعریف این چهار ثابت رخدادهای خورشیدی تعریف کرد:
در ادامه برای محاسبهی زمان equinox از فصل 27 کتاب Astronomical Algorithms کمک گرفته شده و تمام اعداد و ارقام و جداولی را که ملاحظه میکنید از این کتاب استخراج شدهاند.
خروجیهای زمانی ستاره شناسی، عموما بر اساس فرمت Julian Date است که آغاز آن 4713BCE January 1, 12 hours GMT است. به همین جهت در انتهای این مباحث، تبدیل Julian Date به DateTime دات نت را نیز ملاحظه میکنید. همچنین باید دقت داشت که خروجی نهایی بر اساس UTC است و برای زمان ایران، باید 3.5 ساعت به آن اضافه شود.
خروجی این الگوریتم را برای سالهای 2014 تا 2022 به صورت ذیل مشاهده میکنید:
برای نمونه زمان محاسبه شدهی 1394/01/01 02:16:29 با زمان رسمی اعلام شدهی ساعت 2 و 15 دقیقه و 10 ثانیه روز شنبه 1 فروردین 1394 و یا برای سال 93 زمان محاسبه شدهی 1392/12/29 20:28:08 با زمان رسمی ساعت ۲۰ و ۲۷ دقیقه و ۷ ثانیه پنجشنبه ۲۹ اسفند ۱۳۹۲، تقریبا برابری میکند.
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید
Equinox.zip
نام علمی لحظهی سال تحویل، Vernal Equinox است. Equinox به معنای نقطهای است که یک فصل، به فصلی دیگر تبدیل میشود:
Equinox واژهای است لاتین به معنای «شبهای مساوی» و به این نکته اشاره دارد که در Equinox، طول شب و روز یکی میشوند. هر سال دارای دو Equinox است: vernal equinox و autumnal equinox (بهاری و پائیزی). البته باید درنظر داشت که Equinox بهاری در نیم کرهی شمالی بیشتر معنا پیدا میکند؛ زیرا در نیم کرهی جنوبی در همین زمان، پائیز شروع میشود.
بنابراین میتوان enum زیر را برای تعریف این چهار ثابت رخدادهای خورشیدی تعریف کرد:
public enum SunEvent { /// <summary> /// march equinox /// </summary> VernalEquinox, /// <summary> /// june solstice /// </summary> SummerSolstice, /// <summary> /// september equinox /// </summary> AutumnalEquinox, /// <summary> /// december solstice /// </summary> WinterSolstice }
در ادامه برای محاسبهی زمان equinox از فصل 27 کتاب Astronomical Algorithms کمک گرفته شده و تمام اعداد و ارقام و جداولی را که ملاحظه میکنید از این کتاب استخراج شدهاند.
/// <summary> /// Based on Jean Meeus book _Astronomical Algorithms_ /// </summary> public static class EquinoxCalculator { /// <summary> /// Degrees to Radians conversion factor. /// </summary> public static readonly double Deg2Radian = Math.PI / 180.0; public static bool ApproxEquals(double d1, double d2) { const double epsilon = 2.2204460492503131E-16; if (d1 == d2) return true; var tolerance = ((Math.Abs(d1) + Math.Abs(d2)) + 10.0) * epsilon; var difference = d1 - d2; return (-tolerance < difference && tolerance > difference); } /// <summary> /// Calculates time of the Equinox and Solstice. /// </summary> /// <param name="year">Year to calculate for.</param> /// <param name="sunEvent">Event to calculate.</param> /// <returns>Date and time event occurs as a fractional Julian Day.</returns> public static DateTime GetSunEventUtc(this int year, SunEvent sunEvent) { double y; double julianEphemerisDay; if (year >= 1000) { y = (Math.Floor((double)year) - 2000) / 1000; switch (sunEvent) { case SunEvent.VernalEquinox: julianEphemerisDay = 2451623.80984 + 365242.37404 * y + 0.05169 * (y * y) - 0.00411 * (y * y * y) - 0.00057 * (y * y * y * y); break; case SunEvent.SummerSolstice: julianEphemerisDay = 2451716.56767 + 365241.62603 * y + 0.00325 * (y * y) - 0.00888 * (y * y * y) - 0.00030 * (y * y * y * y); break; case SunEvent.AutumnalEquinox: julianEphemerisDay = 2451810.21715 + 365242.01767 * y + 0.11575 * (y * y) - 0.00337 * (y * y * y) - 0.00078 * (y * y * y * y); break; case SunEvent.WinterSolstice: julianEphemerisDay = 2451900.05952 + 365242.74049 * y + 0.06223 * (y * y) - 0.00823 * (y * y * y) - 0.00032 * (y * y * y * y); break; default: throw new NotSupportedException(); } } else { y = Math.Floor((double)year) / 1000; switch (sunEvent) { case SunEvent.VernalEquinox: julianEphemerisDay = 1721139.29189 + 365242.13740 * y + 0.06134 * (y * y) - 0.00111 * (y * y * y) - 0.00071 * (y * y * y * y); break; case SunEvent.SummerSolstice: julianEphemerisDay = 1721233.25401 + 365241.72562 * y + 0.05323 * (y * y) - 0.00907 * (y * y * y) - 0.00025 * (y * y * y * y); break; case SunEvent.AutumnalEquinox: julianEphemerisDay = 1721325.70455 + 365242.49558 * y + 0.11677 * (y * y) - 0.00297 * (y * y * y) - 0.00074 * (y * y * y * y); break; case SunEvent.WinterSolstice: julianEphemerisDay = 1721414.39987 + 365242.88257 * y + 0.00769 * (y * y) - 0.00933 * (y * y * y) - 0.00006 * (y * y * y * y); break; default: throw new NotSupportedException(); } } var julianCenturies = (julianEphemerisDay - 2451545.0) / 36525; var w = 35999.373 * julianCenturies - 2.47; var lambda = 1 + 0.0334 * Math.Cos(w * Deg2Radian) + 0.0007 * Math.Cos(2 * w * Deg2Radian); var sumOfPeriodicTerms = getSumOfPeriodicTerms(julianCenturies); return JulianToUtcDate(julianEphemerisDay + (0.00001 * sumOfPeriodicTerms / lambda)); } /// <summary> /// Converts a fractional Julian Day to a .NET DateTime. /// </summary> /// <param name="julianDay">Fractional Julian Day to convert.</param> /// <returns>Date and Time in .NET DateTime format.</returns> public static DateTime JulianToUtcDate(double julianDay) { double a; int month, year; var j = julianDay + 0.5; var z = Math.Floor(j); var f = j - z; if (z >= 2299161) { var alpha = Math.Floor((z - 1867216.25) / 36524.25); a = z + 1 + alpha - Math.Floor(alpha / 4); } else a = z; var b = a + 1524; var c = Math.Floor((b - 122.1) / 365.25); var d = Math.Floor(365.25 * c); var e = Math.Floor((b - d) / 30.6001); var day = b - d - Math.Floor(30.6001 * e) + f; if (e < 14) month = (int)(e - 1.0); else if (ApproxEquals(e, 14) || ApproxEquals(e, 15)) month = (int)(e - 13.0); else throw new NotSupportedException("Illegal month calculated."); if (month > 2) year = (int)(c - 4716.0); else if (month == 1 || month == 2) year = (int)(c - 4715.0); else throw new NotSupportedException("Illegal year calculated."); var span = TimeSpan.FromDays(day); return new DateTime(year, month, (int)day, span.Hours, span.Minutes, span.Seconds, span.Milliseconds, new GregorianCalendar(), DateTimeKind.Utc); } /// <summary> /// These values are from Table 27.C /// </summary> private static double getSumOfPeriodicTerms(double julianCenturies) { return 485 * Math.Cos(Deg2Radian * 324.96 + Deg2Radian * (1934.136 * julianCenturies)) + 203 * Math.Cos(Deg2Radian * 337.23 + Deg2Radian * (32964.467 * julianCenturies)) + 199 * Math.Cos(Deg2Radian * 342.08 + Deg2Radian * (20.186 * julianCenturies)) + 182 * Math.Cos(Deg2Radian * 27.85 + Deg2Radian * (445267.112 * julianCenturies)) + 156 * Math.Cos(Deg2Radian * 73.14 + Deg2Radian * (45036.886 * julianCenturies)) + 136 * Math.Cos(Deg2Radian * 171.52 + Deg2Radian * (22518.443 * julianCenturies)) + 77 * Math.Cos(Deg2Radian * 222.54 + Deg2Radian * (65928.934 * julianCenturies)) + 74 * Math.Cos(Deg2Radian * 296.72 + Deg2Radian * (3034.906 * julianCenturies)) + 70 * Math.Cos(Deg2Radian * 243.58 + Deg2Radian * (9037.513 * julianCenturies)) + 58 * Math.Cos(Deg2Radian * 119.81 + Deg2Radian * (33718.147 * julianCenturies)) + 52 * Math.Cos(Deg2Radian * 297.17 + Deg2Radian * (150.678 * julianCenturies)) + 50 * Math.Cos(Deg2Radian * 21.02 + Deg2Radian * (2281.226 * julianCenturies)) + 45 * Math.Cos(Deg2Radian * 247.54 + Deg2Radian * (29929.562 * julianCenturies)) + 44 * Math.Cos(Deg2Radian * 325.15 + Deg2Radian * (31555.956 * julianCenturies)) + 29 * Math.Cos(Deg2Radian * 60.93 + Deg2Radian * (4443.417 * julianCenturies)) + 28 * Math.Cos(Deg2Radian * 155.12 + Deg2Radian * (67555.328 * julianCenturies)) + 17 * Math.Cos(Deg2Radian * 288.79 + Deg2Radian * (4562.452 * julianCenturies)) + 16 * Math.Cos(Deg2Radian * 198.04 + Deg2Radian * (62894.029 * julianCenturies)) + 14 * Math.Cos(Deg2Radian * 199.76 + Deg2Radian * (31436.921 * julianCenturies)) + 12 * Math.Cos(Deg2Radian * 95.39 + Deg2Radian * (14577.848 * julianCenturies)) + 12 * Math.Cos(Deg2Radian * 287.11 + Deg2Radian * (31931.756 * julianCenturies)) + 12 * Math.Cos(Deg2Radian * 320.81 + Deg2Radian * (34777.259 * julianCenturies)) + 9 * Math.Cos(Deg2Radian * 227.73 + Deg2Radian * (1222.114 * julianCenturies)) + 8 * Math.Cos(Deg2Radian * 15.45 + Deg2Radian * (16859.074 * julianCenturies)); } }
خروجی این الگوریتم را برای سالهای 2014 تا 2022 به صورت ذیل مشاهده میکنید:
2014 -> 1392/12/29 20:28:08 2015 -> 1394/01/01 02:16:29 2016 -> 1395/01/01 08:01:21 2017 -> 1395/12/30 14:00:00 2018 -> 1396/12/29 19:46:10 2019 -> 1398/01/01 01:29:29 2020 -> 1399/01/01 07:21:03 2021 -> 1399/12/30 13:08:41 2022 -> 1400/12/29 19:04:37
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید
Equinox.zip
در سری کار با Postman، یک روش بسیار متداول آزمایش Web APIs را بررسی کردیم. اما ... برای کار آن با مدام نیاز است از این برگه به آن برگه مراجعه کرد و ارتباط دادن درخواستهای متوالی در آن مشکل است. به همین منظور تابحال راهحلهای زیادی برای جایگزین کردن postman ارائه شدهاند که یکی از آنها strest است. این ابزار خط فرمان:
- بسیار سبک ورزن است و تنها نیاز به نصب بستهی npm آنرا دارد.
- با فایلهای متنی معمولی کار میکند که ویرایش و copy/paste در آنها بسیار سادهاست.
- قرار دادن فایلهای نهایی متنی آن در ورژن کنترل بسیار سادهاست.
- امکان نوشتن درخواستهای به هم وابسته و آزمودن نتایج حاصل را دارا است.
- چون یک ابزار خط فرمان است، امکان استفادهی از آن به سادگی در فرآیندههای توسعهی مداوم وجود دارد.
- ابزارهای npm، چندسکویی هستند.
نصب strest
در ادامه قصد داریم مطلب «آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT» را با استفاده از strest بازنویسی کنیم. به همین جهت در ابتدا نیاز است بستهی npm آنرا به صورت سراسری نصب کنیم:
پس از آن فایل جدید JWT.strest.yml را در پوشهای ایجاد کرده و آنرا تکمیل میکنیم. برای اجرای فرامین موجود در آن تنها کافی است دستور strest JWT.strest.yml را درخط فرمان صادر کنیم.
مرحله 1: خاموش کردن بررسی مجوز SSL برنامه
مرحله 2: ایجاد درخواست login و دریافت توکنها
مجوز SSL آزمایشی برنامهی ASP.NET Core ما، از نوع خود امضاء شدهاست. به همین جهت اگر سعی در اجرای strest را با درخواستهای ارسالی به آن داشته باشیم، باشکست مواجه خواهند شد. بنابراین در ابتدا، خاصیت allowInsecure را به true تنظیم میکنیم:
- این تنظیمات با فرمت yaml نوشته میشوند. به همین جهت در اینجا تعداد spaceها مهم است.
- همچنین در ابتدای این تنظیمات، روش تعریف متغیرها را نیز مشاهده میکنید که برای مثال توسط آنها baseUrl تعریف شدهاست.
درست در سطر پس از این تنظیمات، دستور اجرا و اعتبارسنجی درخواست Login را مینویسیم:
توضیحات:
- درخواستها با requests شروع میشوند. سپس ذیل آن میتوان نام چندین درخواست یا request را ذکر کرد که برای مثال نام درخواست تعریف شدهی در اینجا loginRequest است. این نام مهم است؛ از این جهت که با اشارهی به آن میتوان به فیلدهای خروجی response حاصل، در درخواستهای بعدی، دسترسی یافت.
- سپس، آدرس درخواست مشخص شدهاست. در اینجا روش کار با متغیرها را نیز مشاهده میکنید.
- نوع درخواست POST است.
- در ادامه جزئیات اطلاعات ارسالی به سمت سرور باید مشخص شوند. برای مثال در اینجا با فرمت application/json قرار است یک شیء تشکیل شدهی از username و password ارسال شوند.
- در سطر بعدی، خاصیت log با متغیر logResponse مقدار دهی شدهاست. اگر به true تنظیم شود، اصل خروجی response را توسط برنامهی خط فرمان strest میتوان مشاهده کرد. اگر اینکار خروجی را شلوغ کرد، میتوان آنرا به false تنظیم کرد و این خروجی را در فایل strest_history.json نهایی که حاصل از اجرای آزمایشهای تعریف شدهاست، در کنار فایل JWT.strest.yml خود یافت و مشاهده کرد.
- سپس به قسمت آزمودن نتیجهی درخواست میرسیم. در اینجا انتظار داریم که درخواست حاصل که با فرمت json است، دارای دو خاصیت رشتهای access_token و refresh_token باشد.
مرحلهی 3: ذخیره سازی توکنهای دریافتی در متغیرهای سراسری
مرحلهی 3: ذخیره سازی مراحل انجام شده
در حین کار با strest نیازی به ذخیره سازی نتیجهی حاصل از response، در متغیرهای خاصی نیست. برای مثال اگر بخواهیم به نتیجهی حاصل از عملیات لاگین فوق در درخواستهای بعدی دسترسی پیدا کنیم، میتوان نوشت <$ loginRequest.content.access_token $>
در اینجا درج متغیرها توسط <$ $> صورت میگیرد. سپس loginRequest به نام درخواست مرتبط اشاره میکند. خاصیت content.access_token نیز مقدار خاصیت access_token شیء response را بر میگرداند.
همچنین ذخیره سازی مراحل انجام شده نیز نکتهی خاصی را به همراه ندارد. یک تک فایل متنی JWT.strest.yml وجود دارد که آزمایشهای ما در آن درج میشوند.
مرحلهی 4: دسترسی به منابع محافظت شدهی سمت سرور
در ادامه روش تعریف دو درخواست جدید دیگر را در فایل JWT.strest.yml مشاهده میکنید که از نوع Get هستند و به اکشن متدهای محافظت شده ارسال میشوند:
دو نکتهی جدید در اینجا قابل مشاهدهاست:
- چون نیاز است به همراه درخواست خود، هدر اعتبارسنجی مبتنی بر JWT را که به صورت Bearer value است نیز به سمت سرور ارسال کنیم، خاصیت headers را توسط یک name/value مشخص کردهایم. همانطور که عنوان شد در فایلهای yaml، فاصلهها و تو رفتگیها مهم هستند و حتما باید رعایت شوند.
- سپس دومین آزمون نوشته شده را نیز مشاهده میکنید. در قسمت validate، مشخص کردهایم که خاصیت title دریافتی از response باید مساوی مقدار خاصی باشد.
دقیقا همین نکات برای درخواست دوم به MyProtectedAdminApi تکرار شدهاند.
مرحلهی 5: ارسال Refresh token و دریافت یک سری توکن جدید
اکشن متد account/RefreshToken در سمت سرور، نیاز دارد تا یک شیء جیسون با خاصیت refreshToken را دریافت کند. مقدار این خاصیت از طریق response متناظر با درخواست نامدار loginRequest استخراج میشود که در قسمت postData مشخص شدهاست:
در آخر، به قسمت آزمودن نتیجهی درخواست میرسیم. در اینجا انتظار داریم که درخواست حاصل که با فرمت json است، دارای دو خاصیت رشتهای access_token و refresh_token باشد که بیانگر صدور توکنهای جدیدی هستند.
مرحلهی 6: آزمایش توکن جدید دریافتی از سرور
در قسمت قبل، توکنهای جدیدی صادر شدند که اکنون برای کار با آنها میتوان از متغیر refreshTokenRequest.content.access_toke استفاده کرد:
در اینجا با استفاده از توکن جدید درخواست نامدار refreshTokenRequest، آزمون واحد نوشته شده با موفقیت به پایان میرسد (یا باید برسد که اجرای نهایی آزمایشها، آنرا مشخص میکند).
مرحلهی 7: آزمایش منقضی شدن توکنی که در ابتدای کار پس از لاگین دریافت کردیم
اکنون که refresh token صورت گرفتهاست، دیگر نباید بتوانیم از توکن دریافتی پس از لاگین استفاده کنیم و برنامه باید آنرا برگشت بزند:
به همین جهت، درخواستی ارسال شده که به نتیجهی درخواست نامدار loginRequest اشاره میکند. در این حالت برای آزمایش عملیات، اینبار status بازگشتی از سرور که باید 401 باشد، بررسی شدهاست.
مرحلهی 8: آزمایش خروج از سیستم
در اینجا نیاز است به آدرس account/logout، یک کوئری استرینگ را با کلید refreshToken و مقدار ریفرشتوکن دریافتی از درخواست نامدار refreshTokenRequest، به سمت سرور ارسال کنیم:
خروجی آزمایش شدهی در اینجا، دریافت مقدار true از سمت سرور است.
مرحلهی 9: بررسی عدم امکان دسترسی به منابع محافظت شدهی سمت سرور، پس از logout
در مرحلهی قبل، از سیستم خارج شدیم. اکنون میخواهیم بررسی کنیم که آیا توکن دریافتی پیشین هنوز معتبر است یا خیر؟ آیا میتوان هنوز هم به منابع محافظت شده دسترسی یافت یا خیر:
به همین جهت هدر Authorization را با اکسستوکنی که در مرحلهی ریفرشتوکن دریافت کردیم (پیش از logout)، مقدار دهی میکنیم و سپس درخواستی را به یک منبع محافظت شده ارسال میکنیم. نتیجهی حاصل باید status code ای مساوی 401 داشته باشد که به معنای برگشت خوردن آن است
مرحلهی 10: اجرای تمام آزمونهای واحد نوشته شده
همانطور که در ابتدای بحث نیز عنوان شد فقط کافی است دستور strest JWT.strest.yml را در خط فرمان اجرا کنیم تا آزمونهای ما به ترتیب اجرا شوند:
فایل نهایی این آزمایش را در اینجا میتوانید مشاهده میکنید.
- بسیار سبک ورزن است و تنها نیاز به نصب بستهی npm آنرا دارد.
- با فایلهای متنی معمولی کار میکند که ویرایش و copy/paste در آنها بسیار سادهاست.
- قرار دادن فایلهای نهایی متنی آن در ورژن کنترل بسیار سادهاست.
- امکان نوشتن درخواستهای به هم وابسته و آزمودن نتایج حاصل را دارا است.
- چون یک ابزار خط فرمان است، امکان استفادهی از آن به سادگی در فرآیندههای توسعهی مداوم وجود دارد.
- ابزارهای npm، چندسکویی هستند.
نصب strest
در ادامه قصد داریم مطلب «آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT» را با استفاده از strest بازنویسی کنیم. به همین جهت در ابتدا نیاز است بستهی npm آنرا به صورت سراسری نصب کنیم:
npm i -g @strest/cli
مرحله 1: خاموش کردن بررسی مجوز SSL برنامه
مرحله 2: ایجاد درخواست login و دریافت توکنها
مجوز SSL آزمایشی برنامهی ASP.NET Core ما، از نوع خود امضاء شدهاست. به همین جهت اگر سعی در اجرای strest را با درخواستهای ارسالی به آن داشته باشیم، باشکست مواجه خواهند شد. بنابراین در ابتدا، خاصیت allowInsecure را به true تنظیم میکنیم:
version: 2 variables: baseUrl: https://localhost:5001/api logResponse: false allowInsecure: true
- همچنین در ابتدای این تنظیمات، روش تعریف متغیرها را نیز مشاهده میکنید که برای مثال توسط آنها baseUrl تعریف شدهاست.
درست در سطر پس از این تنظیمات، دستور اجرا و اعتبارسنجی درخواست Login را مینویسیم:
requests: loginRequest: request: url: <$ baseUrl $>/account/login method: POST postData: mimeType: application/json text: username: "Vahid" password: "1234" log: <$ logResponse $> validate: - jsonpath: content.access_token type: [string] - jsonpath: content.refresh_token type: [string]
- درخواستها با requests شروع میشوند. سپس ذیل آن میتوان نام چندین درخواست یا request را ذکر کرد که برای مثال نام درخواست تعریف شدهی در اینجا loginRequest است. این نام مهم است؛ از این جهت که با اشارهی به آن میتوان به فیلدهای خروجی response حاصل، در درخواستهای بعدی، دسترسی یافت.
- سپس، آدرس درخواست مشخص شدهاست. در اینجا روش کار با متغیرها را نیز مشاهده میکنید.
- نوع درخواست POST است.
- در ادامه جزئیات اطلاعات ارسالی به سمت سرور باید مشخص شوند. برای مثال در اینجا با فرمت application/json قرار است یک شیء تشکیل شدهی از username و password ارسال شوند.
- در سطر بعدی، خاصیت log با متغیر logResponse مقدار دهی شدهاست. اگر به true تنظیم شود، اصل خروجی response را توسط برنامهی خط فرمان strest میتوان مشاهده کرد. اگر اینکار خروجی را شلوغ کرد، میتوان آنرا به false تنظیم کرد و این خروجی را در فایل strest_history.json نهایی که حاصل از اجرای آزمایشهای تعریف شدهاست، در کنار فایل JWT.strest.yml خود یافت و مشاهده کرد.
- سپس به قسمت آزمودن نتیجهی درخواست میرسیم. در اینجا انتظار داریم که درخواست حاصل که با فرمت json است، دارای دو خاصیت رشتهای access_token و refresh_token باشد.
مرحلهی 3: ذخیره سازی توکنهای دریافتی در متغیرهای سراسری
مرحلهی 3: ذخیره سازی مراحل انجام شده
در حین کار با strest نیازی به ذخیره سازی نتیجهی حاصل از response، در متغیرهای خاصی نیست. برای مثال اگر بخواهیم به نتیجهی حاصل از عملیات لاگین فوق در درخواستهای بعدی دسترسی پیدا کنیم، میتوان نوشت <$ loginRequest.content.access_token $>
در اینجا درج متغیرها توسط <$ $> صورت میگیرد. سپس loginRequest به نام درخواست مرتبط اشاره میکند. خاصیت content.access_token نیز مقدار خاصیت access_token شیء response را بر میگرداند.
همچنین ذخیره سازی مراحل انجام شده نیز نکتهی خاصی را به همراه ندارد. یک تک فایل متنی JWT.strest.yml وجود دارد که آزمایشهای ما در آن درج میشوند.
مرحلهی 4: دسترسی به منابع محافظت شدهی سمت سرور
در ادامه روش تعریف دو درخواست جدید دیگر را در فایل JWT.strest.yml مشاهده میکنید که از نوع Get هستند و به اکشن متدهای محافظت شده ارسال میشوند:
myProtectedApiRequest: request: url: <$ baseUrl $>/MyProtectedApi method: GET headers: - name: Authorization value: Bearer <$ loginRequest.content.access_token $> log: <$ logResponse $> validate: - jsonpath: content.title expect: "Hello from My Protected Controller! [Authorize]" mProtectedAdminApiRequest: request: url: <$ baseUrl $>/MyProtectedAdminApi method: GET headers: - name: Authorization value: Bearer <$ loginRequest.content.access_token $> log: <$ logResponse $> validate: - jsonpath: content.title expect: "Hello from My Protected Admin Api Controller! [Authorize(Policy = CustomRoles.Admin)]"
- چون نیاز است به همراه درخواست خود، هدر اعتبارسنجی مبتنی بر JWT را که به صورت Bearer value است نیز به سمت سرور ارسال کنیم، خاصیت headers را توسط یک name/value مشخص کردهایم. همانطور که عنوان شد در فایلهای yaml، فاصلهها و تو رفتگیها مهم هستند و حتما باید رعایت شوند.
- سپس دومین آزمون نوشته شده را نیز مشاهده میکنید. در قسمت validate، مشخص کردهایم که خاصیت title دریافتی از response باید مساوی مقدار خاصی باشد.
دقیقا همین نکات برای درخواست دوم به MyProtectedAdminApi تکرار شدهاند.
مرحلهی 5: ارسال Refresh token و دریافت یک سری توکن جدید
اکشن متد account/RefreshToken در سمت سرور، نیاز دارد تا یک شیء جیسون با خاصیت refreshToken را دریافت کند. مقدار این خاصیت از طریق response متناظر با درخواست نامدار loginRequest استخراج میشود که در قسمت postData مشخص شدهاست:
refreshTokenRequest: request: url: <$ baseUrl $>/account/RefreshToken method: POST postData: mimeType: application/json text: refreshToken: <$ loginRequest.content.refresh_token $> log: <$ logResponse $> validate: - jsonpath: content.access_token type: [string] - jsonpath: content.refresh_token type: [string]
مرحلهی 6: آزمایش توکن جدید دریافتی از سرور
در قسمت قبل، توکنهای جدیدی صادر شدند که اکنون برای کار با آنها میتوان از متغیر refreshTokenRequest.content.access_toke استفاده کرد:
myProtectedApiRequestWithNewToken: request: url: <$ baseUrl $>/MyProtectedApi method: GET headers: - name: Authorization value: Bearer <$ refreshTokenRequest.content.access_token $> log: <$ logResponse $> validate: - jsonpath: content.title expect: "Hello from My Protected Controller! [Authorize]"
مرحلهی 7: آزمایش منقضی شدن توکنی که در ابتدای کار پس از لاگین دریافت کردیم
اکنون که refresh token صورت گرفتهاست، دیگر نباید بتوانیم از توکن دریافتی پس از لاگین استفاده کنیم و برنامه باید آنرا برگشت بزند:
myProtectedApiRequestWithOldToken: request: url: <$ baseUrl $>/MyProtectedApi method: GET headers: - name: Authorization value: Bearer <$ loginRequest.content.access_token $> log: <$ logResponse $> validate: - jsonpath: status expect: 401
مرحلهی 8: آزمایش خروج از سیستم
در اینجا نیاز است به آدرس account/logout، یک کوئری استرینگ را با کلید refreshToken و مقدار ریفرشتوکن دریافتی از درخواست نامدار refreshTokenRequest، به سمت سرور ارسال کنیم:
logoutRequest: request: url: <$ baseUrl $>/account/logout method: GET headers: - name: Authorization value: Bearer <$ refreshTokenRequest.content.access_token $> queryString: - name: refreshToken value: <$ refreshTokenRequest.content.refresh_token $> log: <$ logResponse $> validate: - jsonpath: content expect: true
مرحلهی 9: بررسی عدم امکان دسترسی به منابع محافظت شدهی سمت سرور، پس از logout
در مرحلهی قبل، از سیستم خارج شدیم. اکنون میخواهیم بررسی کنیم که آیا توکن دریافتی پیشین هنوز معتبر است یا خیر؟ آیا میتوان هنوز هم به منابع محافظت شده دسترسی یافت یا خیر:
myProtectedApiRequestWithNewTokenAfterLogout: request: url: <$ baseUrl $>/MyProtectedApi method: GET headers: - name: Authorization value: Bearer <$ refreshTokenRequest.content.access_token $> log: <$ logResponse $> validate: - jsonpath: status expect: 401
مرحلهی 10: اجرای تمام آزمونهای واحد نوشته شده
همانطور که در ابتدای بحث نیز عنوان شد فقط کافی است دستور strest JWT.strest.yml را در خط فرمان اجرا کنیم تا آزمونهای ما به ترتیب اجرا شوند:
فایل نهایی این آزمایش را در اینجا میتوانید مشاهده میکنید.
نظرات مطالب
ایجاد نقشه سایت (Site Map) داینامیک
من از این روش (تولید فایل XML به صورت پویا از طریق sitemap.ashx ) استفاده کردم ولی متاسفانه خطای زیر رو میده (از دات نت 4.5 استفاده میکنم )
Type 'sitemap' does not inherit from 'System.Web.UI.Page'.
از EF Core 2.1 به بعد، قابلیت جدیدی تحت عنوان «تبدیلگرهای مقدار»، به آن اضافه شدهاست. برای مثال در EF Core، زمانیکه اطلاعات Enums، در بانک اطلاعاتی ذخیره میشوند، معادل عددی آنها درج خواهند شد. اگر علاقمند باشید تا بجای این مقادیر عددی دقیقا همان رشتهی تعریف کنندهی Enum درج شود، میتوان یک «تبدیلگر مقدار» را برای آن نوشت. برای مثال در موجودیت Rider زیر، خاصیت Mount از نوع یک enum است.
برای اینکه در حین درج رکوردهای Rider در بانک اطلاعاتی دقیقا از مقادیر رشتهای EquineBeast استفاده شود، میتوان به صورت زیر عمل کرد:
در اینجا در حین تعریف جزئیات نگاشت یک مدل میتوان متد جدید HasConversion را نیز فراخوانی کرد. پارامتر اول آن، روش تبدیل مقدار enum را به یک رشته، جهت درج در بانک اطلاعاتی و پارامتر دوم آن، روش تبدیل مقدار رشتهای خوانده شدهی از بانک اطلاعاتی را جهت وهله سازی یک Rider داری خاصیت enum، مشخص میکند.
نکته 1: مقادیر نال، هیچگاه به تبدیلگرهای مقدار، ارسال نمیشوند. اینکار پیاده سازی آنها را سادهتر میکند و همچنین میتوان آنها را بین خواص نالپذیر و نالنپذیر، به اشتراک گذاشت. بنابراین برای مقادیر نال نمیتوان تبدیلگر نوشت.
نکته 2: کاری که در متد HasConversion فوق انجام شدهاست، در حقیقت وهله سازی ضمنی یک ValueConverter و استفاده از آن است. میتوان اینکار را به صورت صریح نیز انجام داد:
مزیت اینکار این است که اگر قرار شد برای چندین خاصیت از تبدیلگر مقدار مشابهی استفاده کنیم، میتوان از یک converter تعریف شده بجای تکرار کدهای آن استفاده کرد.
تبدیلگرهای مقدار توکار EF Core
برای بسیاری از اعمال متداول، در فضای نام Microsoft.EntityFrameworkCore.Storage.ValueConversion، تعدادی تبدیلگر مقدار تدارک دیده شدهاند که به این شرح میباشند:
BoolToZeroOneConverter: تبدیلگر bool به صفر و یک
BoolToStringConverter: تبدیلگر bool به Y و یا N
BoolToTwoValuesConverter: تبدیلگر bool به دو مقداری دلخواه
BytesToStringConverter: تبدیلگر آرایهای از بایتها به یک رشتهی Base64-encoded
CastingConverter: تبدیلگر یک نوع به نوعی دیگر
CharToStringConverter: تبدیلگر char به string
DateTimeOffsetToBinaryConverter: تبدیلگر DateTimeOffset به یک مقدار 64 بیتی باینری
DateTimeOffsetToBytesConverter: تبدیلگر DateTimeOffset به آرایهای از بایتها
DateTimeOffsetToStringConverter: تبدیلگر DateTimeOffset به رشته
DateTimeToBinaryConverter: تبدیلگر DateTime به یک مقدار 64 بیتی با درج DateTimeKind
DateTimeToStringConverter: تبدیلگر DateTime به یک رشته
DateTimeToTicksConverter: تبدیلگر DateTime به ticks آن
EnumToNumberConverter: تبدیلگر Enum به عدد متناظر با آن
EnumToStringConverter: تبدیلگر Enum به رشته
GuidToBytesConverter: تبدیلگر Guid به آرایهای از بایتها
GuidToStringConverter: تبدیلگر Guid به رشته
NumberToBytesConverter: تبدیلگر اعداد به آرایهای از بایتها
NumberToStringConverter: تبدیلگر اعداد به رشته
StringToBytesConverter: تبدیلگر رشته به آرایهای از بایتهای UTF8 معادل آن
TimeSpanToStringConverter: تبدیلگر TimeSpan به رشته
TimeSpanToTicksConverter: تبدیلگر TimeSpan به ticks آن
برای نمونه در این لیست، EnumToStringConverter نیز وجود دارد. بنابراین نیازی به تعریف دستی آن مانند مثال ابتدای بحث نیست و میتوان به صورت زیر از آن استفاده کرد:
نکته: تمام تبدیل کنندههای مقدار توکار EF Core، بدون حالت هستند. بنابراین میتوان یک تک وهلهی از آنها را بین چندین خاصیت به اشتراک گذاشت.
تعیین نوع تبدیلگر مقدار، جهت ساده سازی تعاریف
برای حالاتی که تبدیلگر مقدار توکاری تعریف شدهاست، صرفا تعریف نوع تبدیل، کفایت میکند:
برای نمونه در اینجا با ذکر نوع رشته، تبدیل enum به string به صورت خودکار انجام خواهد شد. معادل اینکار، تعریف نوع سمت بانک اطلاعاتی این خاصیت است:
در این حالت حتی نیازی به تعریف HasConversion هم نیست.
نوشتن تبدیلگر خودکار مقادیر خواص، به نمونهای رمزنگاری شده
پس از آشنایی با مفهوم «تبدیلگرهای مقدار» در +EF Core 2.1، اکنون میتوانیم یک نمونهی سفارشی از آنرا نیز طراحی کنیم:
در اینجا معکوس کردن رشتهها به عنوان الگوریتم سادهی رمزنگاری اطلاعات انتخاب شدهاست. نحوهی اعمال این ValueConverter جدید را نیز ملاحظه میکنید.
میتوان قسمت HasConversion را به صورت زیر خودکار کرد:
ابتدا یک Attribute جدید را به نام Encrypted به برنامه اضافه میکنیم:
هدف از این Attribute خالی، صرفا نشانه گذاری خاصیتهایی است که قرار است به صورت رمزنگاری شده در بانک اطلاعاتی ذخیره شوند؛ مانند خاصیت Value زیر:
پس از آن، متد OnModelCreating را به صورت زیر اصلاح میکنیم تا به کمک Reflection و اطلاعات موجودیتهای ثبت شدهی در سیستم، متد SetValueConverter را بر روی خواصی که دارای EncryptedAttribute هستند، به صورت خودکار فراخوانی کند:
تاثیر ValueConverterها بر روی اعمال متداول کار با بانک اطلاعاتی
از دیدگاه برنامه، ValueConverterهای تعریف شده، هیچگونه تاثیری را بر روی کوئری نوشتن و یا ثبت و ویرایش اطلاعات ندارند و عملکرد آنها کاملا از دیدگاه سایر قسمتهای برنامه مخفی است. برای مثال در برنامه، فرمان به روز رسانی خاصیت Value را با مقدار .A new value to test صادر کردهایم (مقدار دهی متداول)، اما همانطور که ملاحظه میکنید، نمونهی رمزنگاری شدهی آن به صورت خودکار در بانک اطلاعاتی درج شدهاست (پارامتر p0):
و یا کوئری زیر
به این نحو ترجمه خواهد شد:
یعنی نیازی نیست تا مقداری را که در حال جستجوی آن هستیم، خودمان به صورت دستی رمزنگاری کرده و سپس در کوئری قرار دهیم. اینکار به صورت خودکار انجام میشود.
public class Rider { public int Id { get; set; } public EquineBeast Mount { get; set; } } public enum EquineBeast { Donkey, Mule, Horse, Unicorn }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); }
نکته 1: مقادیر نال، هیچگاه به تبدیلگرهای مقدار، ارسال نمیشوند. اینکار پیاده سازی آنها را سادهتر میکند و همچنین میتوان آنها را بین خواص نالپذیر و نالنپذیر، به اشتراک گذاشت. بنابراین برای مقادیر نال نمیتوان تبدیلگر نوشت.
نکته 2: کاری که در متد HasConversion فوق انجام شدهاست، در حقیقت وهله سازی ضمنی یک ValueConverter و استفاده از آن است. میتوان اینکار را به صورت صریح نیز انجام داد:
var converter = new ValueConverter<EquineBeast, string>( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion(converter);
تبدیلگرهای مقدار توکار EF Core
برای بسیاری از اعمال متداول، در فضای نام Microsoft.EntityFrameworkCore.Storage.ValueConversion، تعدادی تبدیلگر مقدار تدارک دیده شدهاند که به این شرح میباشند:
BoolToZeroOneConverter: تبدیلگر bool به صفر و یک
BoolToStringConverter: تبدیلگر bool به Y و یا N
BoolToTwoValuesConverter: تبدیلگر bool به دو مقداری دلخواه
BytesToStringConverter: تبدیلگر آرایهای از بایتها به یک رشتهی Base64-encoded
CastingConverter: تبدیلگر یک نوع به نوعی دیگر
CharToStringConverter: تبدیلگر char به string
DateTimeOffsetToBinaryConverter: تبدیلگر DateTimeOffset به یک مقدار 64 بیتی باینری
DateTimeOffsetToBytesConverter: تبدیلگر DateTimeOffset به آرایهای از بایتها
DateTimeOffsetToStringConverter: تبدیلگر DateTimeOffset به رشته
DateTimeToBinaryConverter: تبدیلگر DateTime به یک مقدار 64 بیتی با درج DateTimeKind
DateTimeToStringConverter: تبدیلگر DateTime به یک رشته
DateTimeToTicksConverter: تبدیلگر DateTime به ticks آن
EnumToNumberConverter: تبدیلگر Enum به عدد متناظر با آن
EnumToStringConverter: تبدیلگر Enum به رشته
GuidToBytesConverter: تبدیلگر Guid به آرایهای از بایتها
GuidToStringConverter: تبدیلگر Guid به رشته
NumberToBytesConverter: تبدیلگر اعداد به آرایهای از بایتها
NumberToStringConverter: تبدیلگر اعداد به رشته
StringToBytesConverter: تبدیلگر رشته به آرایهای از بایتهای UTF8 معادل آن
TimeSpanToStringConverter: تبدیلگر TimeSpan به رشته
TimeSpanToTicksConverter: تبدیلگر TimeSpan به ticks آن
برای نمونه در این لیست، EnumToStringConverter نیز وجود دارد. بنابراین نیازی به تعریف دستی آن مانند مثال ابتدای بحث نیست و میتوان به صورت زیر از آن استفاده کرد:
var converter = new EnumToStringConverter<EquineBeast>(); modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion(converter);
تعیین نوع تبدیلگر مقدار، جهت ساده سازی تعاریف
برای حالاتی که تبدیلگر مقدار توکاری تعریف شدهاست، صرفا تعریف نوع تبدیل، کفایت میکند:
modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion<string>();
public class Rider { public int Id { get; set; } [Column(TypeName = "nvarchar(24)")] public EquineBeast Mount { get; set; } }
نوشتن تبدیلگر خودکار مقادیر خواص، به نمونهای رمزنگاری شده
پس از آشنایی با مفهوم «تبدیلگرهای مقدار» در +EF Core 2.1، اکنون میتوانیم یک نمونهی سفارشی از آنرا نیز طراحی کنیم:
namespace DbConfig.Web.DataLayer.Context { public class MyAppContext : DbContext { // … protected override void OnModelCreating(ModelBuilder builder) { var encryptedConverter = new ValueConverter<string, string>( convertToProviderExpression: v => new string(v.Reverse().ToArray()), // encrypt convertFromProviderExpression: v => new string(v.Reverse().ToArray()) // decrypt ); // Custom application mappings builder.Entity<ConfigurationValue>(entity => { entity.Property(e => e.Value).IsRequired().HasConversion(encryptedConverter); }); } } }
میتوان قسمت HasConversion را به صورت زیر خودکار کرد:
ابتدا یک Attribute جدید را به نام Encrypted به برنامه اضافه میکنیم:
using System; namespace Test { [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class EncryptedAttribute : Attribute { } }
namespace DbConfig.Web.DomainClasses { public class ConfigurationValue { public int Id { get; set; } public string Key { get; set; } [Encrypted] public string Value { get; set; } } }
namespace DbConfig.Web.DataLayer.Context { public class MyAppContext : DbContext { protected override void OnModelCreating(ModelBuilder builder) { var encryptedConverter = new ValueConverter<string, string>( convertToProviderExpression: v => new string(v.Reverse().ToArray()), // encrypt convertFromProviderExpression: v => new string(v.Reverse().ToArray()) // decrypt ); foreach (var entityType in builder.Model.GetEntityTypes()) { foreach (var property in entityType.GetProperties()) { var attributes = property.PropertyInfo.GetCustomAttributes(typeof(EncryptedAttribute), false); if (attributes.Any()) { property.SetValueConverter(encryptedConverter); } } } }
از دیدگاه برنامه، ValueConverterهای تعریف شده، هیچگونه تاثیری را بر روی کوئری نوشتن و یا ثبت و ویرایش اطلاعات ندارند و عملکرد آنها کاملا از دیدگاه سایر قسمتهای برنامه مخفی است. برای مثال در برنامه، فرمان به روز رسانی خاصیت Value را با مقدار .A new value to test صادر کردهایم (مقدار دهی متداول)، اما همانطور که ملاحظه میکنید، نمونهی رمزنگاری شدهی آن به صورت خودکار در بانک اطلاعاتی درج شدهاست (پارامتر p0):
Executed DbCommand (22ms) [Parameters=[@p1='1', @p0='.tset ot eulav wen A' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='180'] SET NOCOUNT ON; UPDATE [Configurations] SET [Value] = @p0 WHERE [Id] = @p1; SELECT @@ROWCOUNT;
و یا کوئری زیر
db.Set<ConfigurationValue>().Where(x => x.Value.EndsWith("world!"))
SELECT [x].[Id], [x].[Key], [x].[Value] FROM [Configurations] AS [x] WHERE RIGHT([x].[Value], LEN(N'world!')) = N'!dlrow'
اشتراکها
C یک زبان سطح پایین نیست
- WMI Provider MSI still failing to install in 16.6
- View History on context menu in Solution Explorer doesn't do anything
- Cannot generate shim for X509Certificate2 with Visual Studio 2019 16.6.0
- Add Controller and Add New Scaffolded Item dialogs are not showing all data contexts after upgrading Visual Studio Enterprise 16.5.6->16.6.0
- Cannot open new json file
- About Microsoft Visual Studio frozen.
- Visual Studio 2019 16.6.0 Microsoft Fakes Issue
- VSSDK IVsHierarchy Regression in VS 16.6.x
- Windows 10 SDK (10.0.19041.1)- ARM64 memcpy crashes when accessing unaligned uncached memory
- Add script to SQL Server Database project does not open User Scripts list
- Fakes generation with ref argument
- Frequent soft hang with Code Analysis callstack in Open Folder project
- Visual Studio Class Designer dark theme support
- Added support for Text Template Transformation Toolkit (T4) in .NET Core projects
- Separate IntelliCode team completions model acquisition from model production.
- Addressed an issue where users may have experienced critical update or installation failures due to the WMIProvider package that would block use of the IDE. Failures in this component no longer block use of the IDE.
- Fixed a problem causing the product to stop responding when working with Xamarin projects on certain scenarios.
- Fixed a bug where VS would crash when attempting to decrypt an invalid UWP code-signing certificate
پاسخ به بازخوردهای پروژهها
موقع ویرایش کاربر خطا داره
بله قسمت ویرایش و حذف کاربران باقی مانده . تکمیل میکنم.
خطا مربوط به Mapping بین ویو مدل و مدل است.البته به صورت سراسری یک ValueConverter برای تبدیل DateTime به string معرفی کردم ولی خطای Type 'System.String' does not have a default constructor
داده شد.برای رفع خطا فعلا این تبدیلگر را غیر فعال کرده ام.