کتاب C# 11 and .NET 7 – Modern Cross-Platform Development, 7th Edition
(سی شارپ 11 و دات نت 7، مبانی توسعه چند سکویی مدرن، ویرایش هفتم)،
راهنمایی قابل دسترس برای برنامه نویسان مبتدی تا متوسط برای مفاهیم،
کاربردهای دنیای واقعی و جدیدترین ویژگیهای C# 11 و NET 7. به همراه
تمرینات عملی با استفاده از Visual Studio 2022 و Visual Studio Code است.
جدیدترین نسخه این کتاب به طور گسترده ای بازنگری شده است تا تمامی
ویژگیهای جدید ارائه شده با سی شارپ 11 و دات نت 7 را در خود جای دهد.
- شاید جالب باشد بدانید که EF-Core با دانت 4x هم قابل استفادهاست. البته تا EF Core 3x بر اساس NET Standard 2.0. کامپایل شده و با دات نت 4x سازگاری دارد. اما EF Core 5x بر اساس NET Standard 2.1. کامپایل شده و با دات نت 4x دیگر سازگار نیست.
- با توجه به اینکه عملیات انجام شده در سطح کلاینت انجام میشود، میتوان معادل آنرا با AutoMapper هم انجام داد.
- و یا میتوان با استفاده از change tracker این تغییرات را اعمال کرد.
یک نکتهی تکمیلی: اضافه شدن متد TryGetNonEnumeratedCount به دات نت 6
public static bool TryGetNonEnumeratedCount(this IEnumerable<T> source, out int count);
زمانیکه متد ()Count را بر روی یک <IEnumerable<T فراخوانی میکنیم، دو کار ممکن رخ میدهند:
الف) کتابخانهی LINQ سعی میکند تا این نوع را به یکی از انواع مشتق شدهی از IEnumerable که دارای خاصیت Count است (مانند IList و امثال آن)، تبدیل کند تا بر این اساس بتواند به سرعت تعداد عناصر آنرا تشخیص دهد.
ب) اگر حالت الف میسر نشد، تمام عناصر IEnumerable را جهت یافتن تعداد آنها، پیمایش و شمارش میکند که این عملیات برای مجموعههای بزرگ میتواند بسیار زمانبر باشد.
اکنون در دات نت 6، حالت امن گزینهی الف، با ارائهی متد الحاقی جدید TryGetNonEnumeratedCount، میسر شدهاست. یعنی اگر مجموعهی مدنظر به همراه خاصیت Count بود، بلافاصله از آن استفاده کرده و خروجی out آنرا ارائه میدهد، در غیراینصورت false را بر میگرداند و مقدار count صفر خواهد بود؛ یعنی حالت ب را دیگر هیچگاه اجرا نخواهد کرد و به این ترتیب میتوان به کنترل بیشتری بر روی API ارائه شده رسید.
if (movies.TryGetNonEnumeratedCount(out int count)) { Console.WriteLine($"The count is {count}"); } else { Console.WriteLine("Could not get a count of movies without enumerating the collection"); }
در قسمت قبل یاد گرفتیم چگونه عملیات پرداخت را انجام دهیم. در این قسمت قصد داریم با تنظیمات پَرباد آشنا شویم.
این تنظیمات در حالت کلی شامل موارد زیر است:
- درگاهها (اجباری)
- HttpContext (اجباری)
- پایگاه داده (اجباری)
- پیامها (اختیاری)
روشهای تنظیم:
- وارد کردن تنظیمات به صورت ثابت (استاتیک)
- تنظیم به صورت داینامیک (برای مثال استفاده از یک منبع، مانند پایگاه داده وب سایت شما)
- تنظیم توسط اینترفیس مایکروسافت IConfiguration
اما قبل از رجوع مستقیم به تنظیمات، بهتر است با نحوه کارکرد آنها آشنا شوید.
پَرباد برای ایجاد و مدیریت تنظیمات و سرویسهای خود، به صورت توکار از تزریق وابستگیها استفاده میکند. بنابراین تنظیم کردن آن به دو حالت امکان پذیر است:
- تنظیم برای پروژههایی که از تزریق وابستگیهای مایکروسافت استفاده میکنند (مانند اپلیکیشنهای ASP.NET CORE ).
- تنظیم برای پروژههایی که از تزریق وابستگیهای مایکروسافت استفاده نمیکنند (مانند Autofac) و یا اینکه اصلا از هیچ تزریق وابستگیای استفاده نمیکنند.
برای روش اول، تنظیمات در حالت کلی به صورت زیر است:
(نمونه مثال در یک اپلیکیشن ASP.NET CORE)
using Parbad.Builder; public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddParbad() // .configurations // .configurations // .configurations }
و برای روش دوم، تنظیمات در حالت کلی به صورت زیر است:
(نمونه مثال در یک اپلیکیشن ASP.NET MVC)
using Parbad.Builder; public class Startup { public void Configuration(IAppBuilder app) { ParbadBuilder.CreateDefaultBuilder() // .configurations // .configurations // .configurations } }
اکنون، با توجه به اینکه با روشهای مختلف تنظیمات آشنا شدید، برای ادامه توضیحات و مثالها (صرفا جهت نوشتن راحتتر این مطلب) از همان روش اول استفاده میکنیم.
تنظیمات درگاهها
طبیعتا شما برای انجام یک پرداخت در درگاه بانک ملت (برای مثال) نیاز به اطلاعاتی مانند نام کاربری و رمز عبور حساب بانکی خود را دارید.
به همین دلیل قبل از انجام هر گونه عملیات پرداخت، ابتدا باید تنظیمات درگاههای مورد استفاده خود را انجام دهید.
به همین دلیل قبل از انجام هر گونه عملیات پرداخت، ابتدا باید تنظیمات درگاههای مورد استفاده خود را انجام دهید.
روش اول: وارد کردن اطلاعات ثابت
نمونه کدهای تنظیم درگاه بانک ملت:
services.AddParbad() .ConfigureGateways(gateways => { gateways .AddMellat() .WithOptions(options => { options.TerminalId = 123; options.UserName = "MyId"; options.UserPassword = "MyPassword"; }); });
نکته: تنظیم سایر درگاهها نیز کاملا مشابه فرمت کدهای بالا است.
روش دوم: تنظیم به صورت داینامیک
برای تنظیم به صورت داینامیک، کلاسی را تعریف کنید که اینترفیس IParbadOptionsProvider را پیادهسازی میکند. مقدار T در این اینترفیس، معادل کلاس مورد نظر جهت تنظیم است.
مثال: تنظیم درگاه ملت توسط یک منبع:
ما قصد داریم اطلاعات مربوط به درگاه بانک ملت را از پایگاه داده فروشگاه خود دریافت کنیم. بنابراین یک منبع را به صورت زیر تعریف میکنیم:
کد بالا اطلاعات مربوط به درگاه بانک ملت را از پایگاه داده (وب سایت شما) دریافت کرده و سپس در متد Provide، آنها را نسبت میدهد.
نکته: ServiceLifetime در اینجا تعیین کننده طول عمر منبع ما است.
public class MellatOptionsProvider : IParbadOptionsProvider<MellatGatewayOptions> { private readonly IMySettingsService _settingsService; public MellatOptionsProvider(IMySettingsService settingsService) { _settingsService = settingsService; } public void Provide(MellatGatewayOptions options) { var settings = _settingsService.GetSettings(); options.TerminalId = settings.TerminalId; options.UserName = settings.UserName; options.UserPassword = settings.UserPassword; } }
نکته: همانطور که در مثال بالا میبینید، در تعریف یک منبع، شما همچنین قادر به
تزریق وابستگیها نیز هستید (در صورت نیاز). بدیهی است که در اینجا، اینترفیس
IMySettingsService توسط تزریق وابستگی اپلیکیشن شما باید ثبت شده باشد، در غیر اینصورت پَرباد قادر به ساخت منبع شما نخواهد بود.
در نهایت منبع را به پَرباد معرفی میکنیم:
services.AddParbad() .ConfigureGateways(gateways => { gateways .AddMellat() .WithOptionsProvider<MellatOptionsProvider>(ServiceLifetime.Transient); });
نمونه مثال کامل را در اینجا میتوانید پیدا کنید.
روش سوم: تنظیم توسط IConfiguration
اگر با اپلیکیشنهای ASP.NET CORE آشنایی داشته باشید، پس قطعا IConfiguration را نیز میشناسید. این اینترفیس به شما کمک میکند تنظیمات مورد نیاز در یک اپلیکیشن را از منابع مختلفی (مانند فایلهای JSON ) دریافت و استفاده کنید.
کد زیر نمونه تنظیم درگاه بانک ملت، با استفاده از IConfiguration و یک فایل JSON است.
و محتوای فایل JSON:
در کد بالا، پَرباد از اینترفیس پیش فرض IHttpContextAccessor در اپلیکیشن ASP.NET CORE استفاده میکند.
نکته: اگر به هر دلیلی، سرور و یا وب سایت شما، ریاستارت شود، اطلاعات موجود در این پایگاه داده ( In-Memory Database ) نیز از بین خواهند رفت. به عبارت دیگر، این پایگاه داده پایدار نیست و صرفا جهت اهداف تست از آن استفاده میشود.
بدیهی است که شما میتوانید این تنظیمات را نادیده گرفته و خودتان مسئولیت نمایش پیام به کاربران را به عهده بگیرید.
services.AddParbad() .ConfigureGateways(gateways => { gateways .AddMellat() .WithConfiguration(IConfiguration.GetSection("Mellat"); });
و محتوای فایل JSON:
"Mellat": { "TerminalId": 123, "UserName": "MyUsername", "UserPassword": "MyPassword" }
تنظیمات HttpContext
پَرباد برای تبادل اطلاعات با درگاههای بانکی، نیاز به یک HttpContext دارد.
ASP.NET WebForms, ASP.NET MVC
ParbadBuilder.CreateDefaultBuilder() .ConfigureHttpContext(builder => builder.UseOwinFromCurrentHttpContext());
در کد بالا، پَرباد HttpContext مورد نیاز خود را توسط Owin تامین میکند. متد UseOwin همچنین شامل گزینههای دیگری جهت تنظیمات بیشتر نیز میباشد.
ASP.NET CORE
services.AddParbad() .ConfigureHttpContext(builder => builder.UseDefaultAspNetCore());
نکته: اگر این اینترفیس قبلا توسط شما و یا اپلیکیشن شما ثبت شده باشد، پَرباد از آن استفاده خواهد کرد؛ در غیر اینصورت، کلاس پیش فرض HttpContextAccessor را به صورت خودکار جهت استفاده ثبت میکند.
تنظیمات پایگاه داده
پایگاه داده استفاده شده در پَرباد سیستم مشهور و شناخته شدهی EntityFrameworkCore است. این بدان معناست که شما میتوانید پایگاه داده مورد نیاز پَرباد را توسط منابع بسیار مختلفی از جمله SQL Server, MySql, Oracle, SQLite و غیره تامین کنید.
SQL Server و InMemory به صورت پیش فرض با پکیج پَرباد در اپلیکیشن شما نصب خواهند شد. اما اگر نیاز به پایگاه دادهی دیگری دارید، میتوانید آن را از بین تامین کنندههای مختلف انتخاب، نصب و استفاده کنید.
نکته: پایگاه داده، برای مصرف و عملکرد داخلی پَرباد است و نه مصرف خارجی در اپلیکیشن شما. در واقع شما نیازی به داشتن اطلاعات درونی پایگاه داده پَرباد ندارید و موارد مهمی مانند کد رهگیری، شماره تراکنش بانکی، مبلغ، نام بانک و غیره را پس از هر عمل پرداخت میتوانید توسط پَرباد دریافت کنید و در پایگاه داده خود برای فاکتور مورد نظر ذخیره کنید.
نمونه کدهای تنظیم را در زیر میتوانید مشاهده کنید:
SQL Server
services.AddParbad() .ConfigureStorage(builder => builder.UseParbadSqlServer("ConnectionString"));
نکته: همانطور که میدانید، متد اصلی دیگری به نام UseSqlServer وجود دارد. تفاوت آن با متد استفاده شدهی در کد بالا این است که UseParbadSqlServer ، به صورت خودکار Migrationهای مرتبط با پروژه پَرباد را نیز اعمال میکند. هر چند که این عمل توسط خود شما نیز امکان پذیر است.
In-Memory Database
services.AddParbad() .ConfigureStorage(builder => builder.UseInMemoryDatabase("MyMemoryName"));
تنظیمات پیامها (اختیاری)
منظور از پیامها، پیامهای متنیای است که پس از انجام عملیاتهای مختلف به شما بازگشت داده میشوند؛ برای مثال: پرداخت با موفقیت انجام شد.
شما میتوانید این پیامها را به شکل زیر تنظیم کنید:
services.AddParbad() .ConfigureMessages(options => { options.PaymentSucceed = "Payment was successful."; options.PaymentFailed = "Payment was not successful."; // other messages... });
نکته: شما همچنین میتوانید از اینترفیس IConfiguration که بالاتر توضیح داده شد نیز برای تنظیم پیامها استفاده کنید.
نمونه پروژهها:
مقالههای مرتبط:
در قسمت اول، درمورد سیستم Cache پیشفرض موجود در Asp.Net Core و مزیتها و معایب آن گفتیم. اگر قسمت اول را نخواندید، قسمت اول مقاله را میتوانید از این لینک بخوانید.
در این قسمت میخواهیم یک پکیج محبوب و کاربردی را برای پیاده سازی کش، در Asp.Net Core را بررسی کنیم.
در دنیای امروز، برنامه نویسی پکیجها و فریمورکها، نقش بسیار مهمی را ایفا میکنند؛ بطوریکه در بسیاری از این موارد، استفاده از این پکیجها، عمل عاقلانهتری نسبت به دوباره نویسی فیچرهای مربوطه است. برای عمل کشینگ در Asp.Net Core نیز پکیجهای فوقالعادهای وجود دارند که در این مقاله، به بررسی و استفاده پکیج این میپردازیم.
در این پکیج، هر یک از متدهای موجود در عملیات کشینگ، بصورت بهینهای تعریف شده و قابل استفادهاند. سیستمی که این پکیج برای کش کردن دادهها استفاده میکند، همان سیستم کش Asp.Net Core هست و بهنوعی، سوار بر این سیستم، قابلیتهای بیشتر و بهتری را ارائه میدهد و این متدها شامل موارد زیر هستند:
- Get/GetAsync(with data retriever)
- Get/GetAsync(without data retriever)
- Set/SetAsync
- Remove/RemoveAsync
- ~~Refresh/RefreshAsync (was removed)~~
- RemoveByPrefix/RemoveByPrefixAsync
- SetAll/SetAllAsync
- GetAll/GetAllAsync
- GetByPrefix/GetByPrefixAsync
- RemoveAll/RemoveAllAsync
- GetCount
- Flush/FlushAsync
- TrySet/TrySetAsync
- GetExpiration/GetExpirationAsync
مفهوم استفاده از این متدها، با همان مفهوم متدهای کش در core، برابری میکند که در قسمت اول این مقاله به آن پرداختیم. همانطور که میبینید، این پکیج از Async Methodها هم پشتیبانی میکند و میتوانید کشهای خود را بصورت Async بنویسید.
یکی از قابلیتهای دیگر این پکیج، سازگاری آن با انواع Cache Providerهای موجود است. بطور خلاصه Cache Providerها، همان ارائه دهندگان حافظهی Ram، در قالبها و ابزارهای مختلف هستند. برخی از اینها با داشتن الگوریتمهای بهینهتر، سرعت بالاتری از رد و بدل کردن اطلاعات در Ram را در اختیار ما قرار میدهند و Local بودن یا Distributed بودن را کنترل میکنند. Cache providerهای گوناگونی وجود دارند که هریک به شکلی کار میکند؛ برای مثال شما میتوانید با Provider ای مستقیما با خود Ram، برای Get و Set کردن کشهای خود در ارتباط باشید و یا در روشی دیگر، از یک دیتابیس(Redis)، جدا از دیتابیس اصلی برنامه که حافظه مصرفی آن Ram هست و منابع حافظه شما را نیز مدیریت میکند، برای کشهای خود استفاده کنید و اطلاعات را بصورت ایندکس گذاری شده در Ram ذخیره کنید که به سرعت واکشی آن میافزاید.
بطور کل Cache Provider هایی که پکیج EasyCaching با آنها سازگار است شامل موارد زیر است:
- In-Memory
- Memcached
- Redis(Based on StackExchange.Redis)
- Redis(Based on csredis)
- SQLite
- Hybrid
- Disk
- LiteDb
یکی دیگر از مزیتهای این پکیج، سازگاری آن با Serializerهای مختلف است. همانطور که میدانید دیتاهای ورودی و خروجی در برنامه، نیاز به Serialize شدن دارند. وقتی میخواهید دیتایی را در دیتابیس ذخیره کنید، آن را در قالب یک شی (Model) از کاربر دریافت میکنید و شما باید برای ذخیره این دیتا، اطلاعات درون شیء را به قالبی که قابل ذخیره شدن باشد، در آورید که این عمل Serialize نام دارد. دقیقا برعکس این روند، بعد از واکشی اطلاعات از دیتابیس، اطلاعات را در قالب اشیایی که قابل نمایش به کاربر باشد (DeSerialize) در میاوریم.
در کش کردن هم چیزی که شما با آن سروکار دارید، دیتا است؛ پس برای ذخیره و واکشی این دیتا، از هر حافظهای، چه دیتابیس و چه Ram، باید از یک Serializer استفاده کنید تا عملیات Serialize و DeSerialize را برایتان انجام دهد. Serializerهای مختلفی وجود دارند که بصورت پکیجهایی ارائه شدهاند و اما Serializer هایی که سیستم EasyCaching آنهارا پشتیبانی میکند، شامل موارد ذیل هستند:
- BinaryFormatter
- MessagePack
- Newtonsoft.Json
- Protobuf
- System.Text.Json
در ادامه به پیاده سازی کش، با استفاده از EasyCaching در سه Provider مختلف از این پکیج میپردازیم.
1_ پروایدر InMemory :
پروایدر InMemory، یک سیستم Local Caching را برای ما به وجود میاورد. در قسمت قبلی مقاله سیستمهای Local(InMemory) و Distributed را بررسی کردیم و تفاوتهای میان آنها را گفتیم.
برای استفاده از پروایدر InMemory در EasyCaching باید پکیج زیر را نصب کنید:
Install-Package EasyCaching.InMemory
در مرحله بعد، کانفیگهای مربوط به این پکیج را در کلاس Startup برنامه خود میاوریم. راحتترین روش افزودن این پکیج به Startup، صرفا افزودن حالت پیشفرض آن به متد ConfigureServices است که به شرح زیر عمل میکنیم:
services.AddEasyCaching(options => { // use memory cache with a simple way options.UseInMemory(); }
این حالت از کانفیگ، پکیج تنظیمات پیشفرض خود پکیج را برای برنامه قرار میدهد؛ شما میتوانید با استفاده از optionهای دیگری که در متد ()UseInMemory وجود دارند، تنظیمات شخصی سازی شده از سیستم کشینگ خود را اعمال کنید.
و تمام. هم اکنون میتوان با استفاده از اینترفیس IEasyCachingProvider که این سرویس در اختیارمان قرار داده و عمل تزریق وابستگی آن در کلاسها و کنترلرهای مان دیتای در حال عبور را کش کنیم. متدهای موجود در این اینترفیس به شرح زیر میباشد :
// تنظیم یک کش با کلید - مقدار - زمان انقضا void Set<T>(string cacheKey, T cacheValue, TimeSpan expiration); Task SetAsync<T>(string cacheKey, T cacheValue, TimeSpan expiration); // تنظیم یک کش با مقدار و زمان انقضا که تایپ مقدار از نوع دیکشنری هست و کلید دیکشنری بعنوان کلید کش قرار میگیرد void SetAll<T>(IDictionary<string, T> value, TimeSpan expiration); Task SetAllAsync<T>(IDictionary<string, T> value, TimeSpan expiration); // تنظیم یک کش با کلید - مقدار - زمان انقضا // اگر کلیدی همنام وجود داشته باشد مقدار نادرست و در غیر اینصورت مقدار نادرست را برمیگرداند bool TrySet<T>(string cacheKey, T cacheValue, TimeSpan expiration); Task<bool> TrySetAsync<T>(string cacheKey, T cacheValue, TimeSpan expiration); // گرفتن یک کش با کلید CacheValue<T> Get<T>(string cacheKey); Task<CacheValue<T>> GetAsync<T>(string cacheKey); // CacheValue<T> Get<T>(string cacheKey, Func<T> dataRetriever, TimeSpan expiration); Task<CacheValue<T>> GetAsync<T>(string cacheKey, Func<Task<T>> dataRetriever, TimeSpan expiration); // گرفتن یک کش با چند کاراکتر پیشین کلید آن // برای مثال یک کلید با نام // MyKey // تنها با داشتن چند حرف اول // MyK // میتوانیم این کش را دریافت کنیم IDictionary<string, CacheValue<T>> GetByPrefix<T>(string prefix); Task<IDictionary<string, CacheValue<T>>> GetByPrefixAsync<T>(string prefix); // IDictionary<string, CacheValue<T>> GetAll<T>(IEnumerable<string> cacheKeys); Task<IDictionary<string, CacheValue<T>>> GetAllAsync<T>(IEnumerable<string> cacheKeys); // گرفتن تعداد کشهای با کاراکترهای پیشین کلید که میان چند کلید یکسان است int GetCount(string prefix = ""); Task<int> GetCountAsync(string prefix = ""); // گرفتن زمان انقضا باقیمانده از یک کش با کلید آن TimeSpan GetExpiration(string cacheKey); Task<TimeSpan> GetExpirationAsync(string cacheKey); // حذف کردن یک کش با کلید void Remove(string cacheKey); Task RemoveAsync(string cacheKey); // حذف کردن یک کش با چند کاراکتر پیشین کلید void RemoveByPrefix(string prefix); Task RemoveByPrefixAsync(string prefix); // حذف کردن چند کش با لیستی از کلیدها void RemoveAll(IEnumerable<string> cacheKeys); Task RemoveAllAsync(IEnumerable<string> cacheKeys); // بررسی وجود یا عدم وجود یک کش با کلید bool Exists(string cacheKey); Task<bool> ExistsAsync(string cacheKey); // حذف کردن همه کشها void Flush(); Task FlushAsync();
همانطور که قبلا گفته شد، سیستم کش، با دیتا مرتبط است و نیازمند یک Object Serializer جهت Serialize کردن اطلاعات ورودی و ذخیره آن در Target Storage مشخص شده است. پکیج EasyCaching برای Providerهای خود، یک Object Serializer پیشفرض قرار دادهاست و تا وقتی که شما آن را طبق نیازی خاص، بصورت سفارشی تغییر نداده باشید، از آن استفاده میکند.
در میان پنج Serializer معرفی شده که EasyCaching آنها را پشتیبانی میکند، BinaryFormatter بصورت پیشفرض در همهی Providerها برقرار است و تا وقتی یک Serializer انتخابی به EasyCaching معرفی نکنید، این پکیج از این Serializer استفاده میکند.
برای استفاده از Serializerهای دیگری که معرفی شده میتوانید از لینکهای زیر کمک بگیرید :
2 - پروایدر Redis :
ردیس، یک دیتابیس Key Value محور هست که محل ذخیره سازی آن Ram است و اطلاعات، بصورت موقت در آن ذخیره میشوند. بطور خلاصه، Key Value یعنی یکبار کلید و مقداری برای آن کلید تعریف میشود و هر وقت نام کلید تعریف شده، صدا زده شد، مقدار نسبت داده شده به آن، در اختیار ما قرار میگیرد. برای مثال کلید "Name" و مقدار "James". با این انتساب، هروقت "Name" فراخوانده شود، مقدار "James" را خواهیم داشت. سیستم Key Value بخاطر عدم پیچیدگی و سادگیای که دارد، بسیار سریع عمل میکند و همچنین ایندکس گذاریهایی که ردیس روی دیتاها انجام میدهد، باعث افزایش سرعت آن نیز خواهد شد که ردیس را به سریعترین دیتابیس Key Value دنیا تبدیل کرده.
در اینجا با توجه به قابلیت هایی که ردیس داراست، یکی از بهترین گزینهها برای انتخاب بعنوان فضای ذخیره سازی کشها بصورت Distributed است.
برای استفاده از این دیتابیس قدرتمند ابتدا باید از طریق یکی از روشهای معمول اقدام به نصب آن کنید. میتوانید فایل نصبی را از وبسایت رسمی آن دانلود کنید و یا یا با استفاده از Docker اقدام به نصب آن نمایید.
پس از نصب این دیتابیس روی سیستم خود ، برای استفاده از آن در EasyCaching ابتدا باید پکیج مورد نیاز را نصب کنید.
Install-Package EasyCaching.Redis
services.AddEasyCaching(option => { option.UseRedis(config => { config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); }); });
با استفاده از متد UseRedis شما قابلیت استفاده از ردیس را در EasyCaching فعال میکنید و سپس باید اطلاعات Host و Port ردیس نصب شدهی روی سیستم خود را به این متد معرفی کنید.
اگر ردیس را بدون تنظیمات شخصی سازی شده و در همان حالت پیشفرض خودش نصب کرده باشید، Host و Port شما مانند نمونه بالا 127.0.0.1 و 6379 خواهد بود و نیازی به تغییر نیست.
در مرحله بعد برای استفاده از پروایدر ردیس ، اینترفیس IRedisCachingProvider در سرتاسر برنامه در دسترس خواهد بود. این اینترفیس علاوه بر اینکه متدهای اصلی موجود در EasyCaching را ساپورت کرده ، بخاطر ساختار دیتابیسی که خود ردیس در اختیار ما قرار میدهد قابلیتهای بیشتری نیز اراعه خواهد داد. این قابلیتها خصیصههای ردیس هست چرا که این دیتابیس هم دقیقا شبیه به ساختار سیستم کش Key , Value را پشتیبانی میکند و در پی آن قابلیت هایی برای مدیریت بهتر کلیدها و مقادیر اراعه میدهد.
اینترفیس IRedisCachingProvider شامل تعداد زیادی از متدها برای پشتیبانی از قابلیتهای ردیس است که در ادامه همه آنهارا نام برده و برخی را توضیح مختصری خواهیم داد:
- متدهای Keys
// حذف کردن یک کلید در صورت وجود bool KeyDel(string cacheKey); Task<bool> KeyDelAsync(string cacheKey); // تنظیم تاریخ انتضا به یک کلید موجود بر حسب ثانیه bool KeyExpire(string cacheKey, int second); Task<bool> KeyExpireAsync(string cacheKey, int second); // بررسی وجود یا عدم وجود یک کلید bool KeyExists(string cacheKey); Task<bool> KeyExistsAsync(string cacheKey); // گرفتن زمان انتقضا باقیمانده یک کلید long TTL(string cacheKey); Task<long> TTLAsync(string cacheKey); // جستجو بین همه کلیدها براساس فیلتر شامل بودن نام کلید از مقدار ورودی List<string> SearchKeys(string cacheKey, int? count = null);
- متدهای String
// افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید long IncrBy(string cacheKey, long value = 1); Task<long> IncrByAsync(string cacheKey, long value = 1); // افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید double IncrByFloat(string cacheKey, double value = 1); Task<double> IncrByFloatAsync(string cacheKey, double value = 1); // تنظیم یک کلید و مقدار وقتی مقدار از نوع رشته باشد bool StringSet(string cacheKey, string cacheValue, TimeSpan? expiration = null, string when = ""); Task<bool> StringSetAsync(string cacheKey, string cacheValue, TimeSpan? expiration = null, string when = ""); // گرفتن کلید و مقدار آن وقتی مقدار از نوع رشته باشد string StringGet(string cacheKey); Task<string> StringGetAsync(string cacheKey); // گرفتن تعداد کاراکترهای مقدار یک کلید وقتی مقدار از نوع رشته باشد long StringLen(string cacheKey); Task<long> StringLenAsync(string cacheKey); // جایگزاری یک رشته درون رشته مقدار یک کلید بعد از شماره کاراکتر مشخص شده در ورودی برای مثال // "Hello World" // 6 , jack // "Hello jack" long StringSetRange(string cacheKey, long offest, string value); Task<long> StringSetRangeAsync(string cacheKey, long offest, string value); // گرفتن یک بازه از رشته مقدار یک کلید با شماره کاراکتر شروع و پایان string StringGetRange(string cacheKey, long start, long end); Task<string> StringGetRangeAsync(string cacheKey, long start, long end);
- متدهای Hashes
// شما میتوانید دو کلید با نامهای یکسان داشته باشید که در کلید تایپ دیکشنری مقدار خود باهم متفاوت هستند bool HMSet(string cacheKey, Dictionary<string, string> vals, TimeSpan? expiration = null); Task<bool> HMSetAsync(string cacheKey, Dictionary<string, string> vals, TimeSpan? expiration = null); // شما میتوانید دو کلید با نامهای یکسان داشته باشید که در ورودی فیلد باهم متفاوت هستند bool HSet(string cacheKey, string field, string cacheValue); Task<bool> HSetAsync(string cacheKey, string field, string cacheValue); // بررسی وجود یا عدم وجود یک کلید و فیلد bool HExists(string cacheKey, string field); Task<bool> HExistsAsync(string cacheKey, string field); // حذف کردن کلیدهای همنام موجود با همه فیلدهای متفاوت در حالت پیشفرض مگر اینکه کلید و نام فیلد را بهمراه آن مشخص کنید long HDel(string cacheKey, IList<string> fields = null); Task<long> HDelAsync(string cacheKey, IList<string> fields = null); // گرفتن مقدار با نام کلید و نام فیلد string HGet(string cacheKey, string field); Task<string> HGetAsync(string cacheKey, string field); // گرفتن فیلد و مقدار با کلید Dictionary<string, string> HGetAll(string cacheKey); Task<Dictionary<string, string>> HGetAllAsync(string cacheKey); // افزودن یک عدد (پیشقرض 1) به مقدار نوع عددی یک کلید و فیلد long HIncrBy(string cacheKey, string field, long val = 1); Task<long> HIncrByAsync(string cacheKey, string field, long val = 1); // گرفتن فیلدهای متفاوت یک کلید List<string> HKeys(string cacheKey); Task<List<string>> HKeysAsync(string cacheKey); // گرفتن تعداد فیلدهای متفاوت یک کلید long HLen(string cacheKey); Task<long> HLenAsync(string cacheKey); // گرفتن مقادیر یک کلید بدون در نظر گرفتن فیلدهای متفاوت List<string> HVals(string cacheKey); Task<List<string>> HValsAsync(string cacheKey); // گرفتن مقدار دیکشنری با کلید و نام فیلدها Dictionary<string, string> HMGet(string cacheKey, IList<string> fields); Task<Dictionary<string, string>> HMGetAsync(string cacheKey, IList<string> fields);
- متدهای List
// گرفتن یک مقدار از لیست مقادیر با شماره ایندکس آن T LIndex<T>(string cacheKey, long index); Task<T> LIndexAsync<T>(string cacheKey, long index); // گرفتن تعداد مقادیر در لیست یک کلید long LLen(string cacheKey); Task<long> LLenAsync(string cacheKey); // گرفتن اولین مقدار از مقادیر یک لیست در یک کلید T LPop<T>(string cacheKey); Task<T> LPopAsync<T>(string cacheKey); // ایجاد یک کلید که لیستی از مقادیر را پشتیبانی میکند و میتوانید هر بار مقدار جدید به لیست آن اضافه کنید long LPush<T>(string cacheKey, IList<T> cacheValues); Task<long> LPushAsync<T>(string cacheKey, IList<T> cacheValues); // گرفتن مقادیر یک لیست از داده بر اساس شماره ایندکس شروع و پایان برای مثال مقادیر ۳ تا ۷ از ۱۰ مقدار List<T> LRange<T>(string cacheKey, long start, long stop); Task<List<T>> LRangeAsync<T>(string cacheKey, long start, long stop); // حذف کردن مقادیر یک لیست بر اساس تعداد وارد شده که بعد از مقدار وارد شده شروع به شمارش میشود long LRem<T>(string cacheKey, long count, T cacheValue); Task<long> LRemAsync<T>(string cacheKey, long count, T cacheValue); // افزودن یک مقدار به لیستی از مقادیر یک کلید با گرفتن شماره ایندکس bool LSet<T>(string cacheKey, long index, T cacheValue); Task<bool> LSetAsync<T>(string cacheKey, long index, T cacheValue); // بررسی میکند که لیست مقداری برای شماره ایندکس شروع و پایان درون خودش دارد یا خیر bool LTrim(string cacheKey, long start, long stop); Task<bool> LTrimAsync(string cacheKey, long start, long stop); // https://redis.io/commands/lpushx long LPushX<T>(string cacheKey, T cacheValue); Task<long> LPushXAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/linsert long LInsertBefore<T>(string cacheKey, T pivot, T cacheValue); Task<long> LInsertBeforeAsync<T>(string cacheKey, T pivot, T cacheValue); // https://redis.io/commands/linsert long LInsertAfter<T>(string cacheKey, T pivot, T cacheValue); Task<long> LInsertAfterAsync<T>(string cacheKey, T pivot, T cacheValue); // https://redis.io/commands/rpushx long RPushX<T>(string cacheKey, T cacheValue); Task<long> RPushXAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/rpush long RPush<T>(string cacheKey, IList<T> cacheValues); Task<long> RPushAsync<T>(string cacheKey, IList<T> cacheValues); // https://redis.io/commands/rpop T RPop<T>(string cacheKey); Task<T> RPopAsync<T>(string cacheKey);
- متدهای Set
// https://redis.io/commands/SAdd long SAdd<T>(string cacheKey, IList<T> cacheValues, TimeSpan? expiration = null); Task<long> SAddAsync<T>(string cacheKey, IList<T> cacheValues, TimeSpan? expiration = null); // https://redis.io/commands/SCard long SCard(string cacheKey); Task<long> SCardAsync(string cacheKey); // https://redis.io/commands/SIsMember bool SIsMember<T>(string cacheKey, T cacheValue); Task<bool> SIsMemberAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/SMembers List<T> SMembers<T>(string cacheKey); Task<List<T>> SMembersAsync<T>(string cacheKey); // https://redis.io/commands/SPop T SPop<T>(string cacheKey); Task<T> SPopAsync<T>(string cacheKey); // https://redis.io/commands/SRandMember List<T> SRandMember<T>(string cacheKey, int count = 1); Task<List<T>> SRandMemberAsync<T>(string cacheKey, int count = 1); // https://redis.io/commands/SRem long SRem<T>(string cacheKey, IList<T> cacheValues = null); Task<long> SRemAsync<T>(string cacheKey, IList<T> cacheValues = null);
- متدهای Stored Set
// https://redis.io/commands/ZAdd long ZAdd<T>(string cacheKey, Dictionary<T, double> cacheValues); Task<long> ZAddAsync<T>(string cacheKey, Dictionary<T, double> cacheValues); // https://redis.io/commands/ZCard long ZCard(string cacheKey); Task<long> ZCardAsync(string cacheKey); // https://redis.io/commands/ZCount long ZCount(string cacheKey, double min, double max); Task<long> ZCountAsync(string cacheKey, double min, double max); // https://redis.io/commands/ZIncrBy double ZIncrBy(string cacheKey, string field, double val = 1); Task<double> ZIncrByAsync(string cacheKey, string field, double val = 1); // https://redis.io/commands/ZLexCount long ZLexCount(string cacheKey, string min, string max); Task<long> ZLexCountAsync(string cacheKey, string min, string max); // https://redis.io/commands/ZRange List<T> ZRange<T>(string cacheKey, long start, long stop); Task<List<T>> ZRangeAsync<T>(string cacheKey, long start, long stop); // https://redis.io/commands/ZRank long? ZRank<T>(string cacheKey, T cacheValue); Task<long?> ZRankAsync<T>(string cacheKey, T cacheValue); // https://redis.io/commands/ZRem long ZRem<T>(string cacheKey, IList<T> cacheValues); Task<long> ZRemAsync<T>(string cacheKey, IList<T> cacheValues); // https://redis.io/commands/ZScore double? ZScore<T>(string cacheKey, T cacheValue); Task<double?> ZScoreAsync<T>(string cacheKey, T cacheValue);
- متدهای Hyperloglog
// https://redis.io/commands/PfAdd bool PfAdd<T>(string cacheKey, List<T> values); Task<bool> PfAddAsync<T>(string cacheKey, List<T> values); // https://redis.io/commands/PfCount long PfCount(List<string> cacheKeys); Task<long> PfCountAsync(List<string> cacheKeys); // https://redis.io/commands/PfMerge bool PfMerge(string destKey, List<string> sourceKeys); Task<bool> PfMergeAsync(string destKey, List<string> sourceKeys);
- متدهای Geo
// https://redis.io/commands/GeoAdd long GeoAdd(string cacheKey, List<(double longitude, double latitude, string member)> values); Task<long> GeoAddAsync(string cacheKey, List<(double longitude, double latitude, string member)> values); // https://redis.io/commands/GeoDist double? GeoDist(string cacheKey, string member1, string member2, string unit = "m"); Task<double?> GeoDistAsync(string cacheKey, string member1, string member2, string unit = "m"); // https://redis.io/commands/GeoHash List<string> GeoHash(string cacheKey, List<string> members); Task<List<string>> GeoHashAsync(string cacheKey, List<string> members); // https://redis.io/commands/GeoPos List<(decimal longitude, decimal latitude)?> GeoPos(string cacheKey, List<string> members); Task<List<(decimal longitude, decimal latitude)?>> GeoPosAsync(string cacheKey, List<string> members);
3 - پروایدر Hybrid :
این پروایدر، روشی از کشینگ را مابین local caching و distributed caching، ارائه میدهد و میتوانید از یک پروایدر Local مثل InMemory و پروایدر Distributed مثل Redis، همزمان باهم استفاده کنید که در یک کانال باهم و در راستای هم کار میکنند.
اما سوال اینجاست که این قابلیت دقیقا چه کاری انجام میدهد؟
همانطور که قبلا گفته شد، کش In-Memory سرعت بالاتری نسبت به کش Distributed دارد؛ اما دچار معایبی در حالت چند سروری هست که این معایب از جمله حذف شدن دیتای یک سرور، در صورت Down شدن آن، Sync نبودن کش سرورها باهم دیگر و دو نسخه، کش کردن دیتا در هر سرور و موارد دیگری که میتوان نام برد. اما از طرفی کش Distributed مشکلات چند سروری را با قرار دادن یک مرکزیت واحد کش در حافظه شبکه شده سرورها برطرف میکند و اطلاعات سرورها، از یک منبع خوانده میشود و طبعا مشکلات In-Memory را نخواهیم داشت؛ اما به دلیل رد و بدل شدن دیتا در محیط شبکه و عمل Serialize , Deserialize که هنگام عبور دیتا روی آن صورت میگیرد، بخشی از سرعت، کاهش خواهد یافت و درنهایت Performance کمتری را نسبت به In-Memory ارائه میدهد.
حالا برای اینکه بتوانیم سیستم کش خودمان را طوری طراحی کنیم که عیبهای (Local)In-Memory و Distributed را نداشته باشیم و بتوانیم از هریک به شکلی درست استفاده کنیم که هم اطلاعاتمان Sync باشد و هم از سرعت بالای In-Memory برخوردار شویم، میتوانیم از پروایدر Hybrid استفاده کنیم.
شیوه کار این پروایدر به این صورت است که وقتی برنامه برای بار اول به کش In-Memory درخواستی را ارسال میکند و کش مورد نظر در آن وجود ندارد، برنامه یک درخواست دیگر را به کش Distributed ارسال میکند و دیتای مورد نظر را به کاربر بازگشت میدهد و علاوه بر آن یک کپی از کش آن دیتا، در کش In-Memory هم ایجاد میکند. با این ساختار از دفعات بعد که کاربر درخواستی را ارسال کند، دیتای درخواستی در In-Memory نیز موجود خواهد بود و سریعتر از بار اول پاسخ را ارسال خواهد کرد.
از طرفی نیز وقتی کاربر دیتای جدیدی را ذخیره میکند، ابتدا آن دیتا در In-Memory کش شده و سپس با درخواست خود پروایدر، در کش Distributed هم اعمال میشود تا در نهایت دیتابیس نیز آن را ذخیره کند.
وقتی این اتفاق میافتد، پروایدر Hybrid با کمک پکیج Bus.Redis به کش In-Memory سرورهای دیگر دستور Pull کردن دیتا کشهای جدید را ارسال میکند و در نهایت همه سرورها نیز به کمک Distributed مرکزی باهم Sync خواهند بود.
برای فعال سازی این پروایدر باید پکیجهای زیر را در برنامه خود نصب کنید:
Install-Package EasyCaching.HybridCache Install-Package EasyCaching.InMemory Install-Package EasyCaching.Redis Install-Package EasyCaching.Bus.Redis
در این مجموعه از پکیجها، از یک پروایدر Local(InMemory) و یک پروایدر distributed(Redis) استفاده شده و همانطور که گفته شد، مدیریت هماهنگ سازی این دو، توسط پکیج دیگری بنام EasyCaching.Bus.Redis صورت میگیرد.
تنظیمات فعالسازی این پروایدر هم متشکل از تنظیمات دو پروایدر In-Memory و Redis، بعلاوه معرفی این دو به هم در متد UseHybrid خواهد بود.
services.AddEasyCaching(option => // local option.UseInMemory("c1"); // distributed option.UseRedis(config => config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); }, "c2"); // combine local and distributed option.UseHybrid(config => // specify the local cache provider name after v0.5.4 config.LocalCacheProviderName = "c1" // specify the distributed cache provider name after v0.5.4 config.DistributedCacheProviderName = "c2" }); // use redis bus .WithRedisBus(busConf => busConf.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379)); }); });
برای استفاده از این پروایدر، متفاوت با پروایدرهای قبلی، باید اینترفیس IHybridCachingProvider را فراخوانی کنیم. متدهای موجود در این اینترفیس، همان متدهایی است که در اینترفیس IEasyCachingProvider وجود دارند و از نظر نام متد و روش استفاده، تفاوتی میان آن نیست.
پیشنهاد شخصی در Distributed Cacheها
همانطور که گفته شد Distributed کشها، گزینه مناسبتری برای برنامههای چند سروری هستند؛ اما در این حالت مواردی مثل Round Trip شبکه و جابجایی اطلاعات در این محیط بعلاوه Serialize , Deserialize هایی که باید انجام شوند دلیلی میشود تا سرعت آن در پاسخ به درخواستهای برنامه، نسبت به حالت تک سروری(In-Memory) کمتر باشد. Hybrid Provider یکی از روشهای حل این مشکل بوده که معرفی کردیم. اما برای اینکه تیر خلاص را به پیکره سیستم Distributed Cache خود بزنید و تریک فنی آخر را نیز روی آن اجرا کنید، پیشنهاد میکنم از پکیج EasyCaching.Extensions.EasyCompressor که بر پایه پکیج EasyCaching نوشته شده استفاده کنید. این پکیج، اطلاعات را قبل از کش شدن، فشرده سازی میکند و حجم اطلاعات را به طور محسوسی کاهش میدهد که میزان فضای اشغالی Ram را کم کرده و همچنین عمل جابجایی اطلاعات را نیز تسریع میبخشد. میتوانید از این پکیج هم در Redis و هم در Hybrid استفاده کنید. چگونگی استفاده از آن نیز در لینک Github ذکر شده موجود است.
معرفی پروژه
تا اینجا با مفاهیمی که برای شروع استفاده حرفهای از کش در پروژهتان نیاز بود، آشنا شدید. در پروژههای واقعی، میتوانیم از این سیستم به روشهای مختلفی در سطوح مختلفی از برنامه استفاده کنیم؛ برای مثال کدهای مربوط به عملیات کش را میتوان بصورت سادهای در هر کنترلر تزریق و در اکشنها استفاده کرد؛ یا از لایه کنترلر، آن را به لایه سرویس منتقل کرد. در روشی دیگر میتوانیم یک Attribute را برای این عمل در نظر بگیریم و یا اینکه آن را بصورت یک Middleware اختصاصی در برنامه پیاده کنیم.
در این پروژه علاوه بر اینکه سعی کردهام استفاده از Providerهای معرفی شده را در محیط واقعیتر پیاده سازی کنم، در هر پروژه از این Solution، کش را به شیوهای متفاوت در لایههای مختلفی از برنامه قرار دادهام تا شما همراهان بتوانید طبق نیازتان از روشی مناسب و بهینه در پروژههای واقعی خود از آن استفاده کنید.
جواب ساده و کوتاه: خیر!
کدمدیریت شدهی شما در هر دو پلتفرم 32 بیتی - x86 و x64 بدون نیاز به هیچگونه تغییری و بدون نگرانی اجرا خواهد شد.
گزیدهای از MSDN :
اگر کد شما 100 درصد مدیریت شده است (managed code ایی که به صورت خالص از دات نت فریم ورک استفاده میکند و هیچگونه وابستگی خارجی دیگری به کتابخانههای دیگر ندارد)، تنها با کپی شدن در یک محیط x64 دارای CLR ایی 64 بیتی (دات نت فریم ورک 64 بیتی)، بدون هیچگونه مشکلی اجرا خواهد شد.
سؤال: چرا و چگونه؟!
کامپایلرهای دات نتی (تفاوتی نمیکند که چه زبانی مورد استفاده باشد)، کد شما را به IL ترجمه میکنند و IL اساسا درکی از پروسسور ندارد. JIT است که در آخرین لحظه در این مورد تصمیم گیری میکند.
این نگرانی از کجا حاصل شده است؟
نگارش R2 ویندوز 2008 سرور، فقط 64 بیتی خواهد بود و ویندوز سرور 2008 فعلی، آخرین سروری از مایکروسافت است که هر دو نسخهی 32 بیتی و 64 بیتی را دارد. بنابراین دیر یا زود تمام برنامه نویسهای ویندوزی "مجبور" خواهند شد دنیای 64 بیتی را تجربه کنند. (البته اگر تاکنون آنرا تجربه نکردهاند)
و البته هنوز یک سری از محیطهای توسعه، کامپایلر مخصوص 64 بیتی ندارند (مانند دلفی که قرار است در طول سال جاری اولین تجربهی 64 بیتی خود را ارائه دهد)
نکته:
در صفحهی build ویژوال استودیو، شما میتوانید نوع پلتفرم مورد نظر را نیز تعیین کنید:
پیش فرض آن بر روی Any CPU است و در این حالت کد کامپایل شدهی شما بدون مشکل بر روی پلتفرمهایی که مشاهده میکنید اجرا خواهد شد و تنها پیشنیاز اجرای آن، نصب نسخهی دات نت فریم ورک مخصوص آن پلتفرم است، بدون اینکه نیاز باشد برنامه نویس نگران جزئیات خاصی در مورد خصوصیات ویژهی آن پلتفرم ویژه باشد.
سؤال: اگر کد ما خالص نبود چطور؟ (منظور اینکه 100 درصد دات نتی نبود)
حالت الف) اگر از کامپوننتهای خارجی استفاده میکنید (حتی اگر 100 درصد دات نتی هم باشند) حتما اطمینان حاصل کنید که برای پلتفرم خاصی کامپایل نشدهاند (همان Any CPU مورد استفاده بوده)، زیرا کد شما که برای تمام CPU ها کامپایل شده، در محیط 64 بیتی، تنها توانایی بارگذاری اسمبلیهای 64 بیتی را خواهد داشت (64 بیتی رفتار میکند) و با مواجه شدن با اسمبلیهایی که برای یک پروسسور خاص دیگر کامپایل شدهاند، با خطای BadImageFormatException خاتمه مییابد.
حالت ب) استفاده از API ویندوز یا DLL های غیر دات نتی
باید با هماهنگی با تولید کنندهی مربوطه حتما از نگارش 64 بیتی استفاده شود و همچنین برنامهی شما باید توانایی استفاده از اشارهگرهای 64 بیتی را داشته باشد. اندازهی نوع دادهای IntPtr در یک محیط 32 بیتی 4 است و در یک محیط 64 بیتی 8 خواهد بود (IntPtr.Size). اگر در حین اجرای ترجمهی API یک کتابخانه به اشتباه بجای استفاده از IntPtr از int استفاده شده باشد، ممکن است کد شما در یک محیط 32 بیتی سالها بدون مشکل اجرا شود، اما در اولین اجرای خود در یک محیط 64 بیتی، کرش خواهد کرد. (بدلیل overflow حاصل)
IntPtr به اندازهی کافی هوشمند است تا سایز خودش را مطابق پلتفرم تنظیم کند و مشکل ساز نشود.
مثال:
[DllImport("kernel32.dll")]
public static extern void GetSystemInfo([MarshalAs(UnmanagedType.Struct)] ref SYSTEM_INFO lpSystemInfo);
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEM_INFO
{
internal _PROCESSOR_INFO_UNION uProcessorInfo;
public uint dwPageSize;
public IntPtr lpMinimumApplicationAddress;
public int lpMaximumApplicationAddress;
public IntPtr dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public ushort dwProcessorLevel;
public ushort dwProcessorRevision;
}
[StructLayout(LayoutKind.Explicit)]
public struct _PROCESSOR_INFO_UNION
{
[FieldOffset(0)]
internal uint dwOemId;
[FieldOffset(0)]
internal ushort wProcessorArchitecture;
[FieldOffset(2)]
internal ushort wReserved;
}
Here's a couple of final thoughts.
- 3 major front end frameworks (Angular, React, and Vue)
- React is the highest growing and highest paying front end framework
- Express is still the dominant Node framework by far
- TypeScript is becoming more and more popular (and it's going to take over...just my thought)
- Tooling is making JavaScript a more evolved and appealing language
- Options to reach almost any platform (desktop, mobile, server, web, hybrid, etc.)
- Graphql is on the rise, especially with the rise of Gatsby.js
Nancy یک فریم ورک سبک برای ساخت سرویسهای مبتنی بر HTTP بر روی .Net و Mono و در واقع پیاده سازی Sinatra در Ruby برای .net است. با استفاده از این کتابخانه شما میتوانید به سادگی درخواستهای مختلف کاربران را از طریق وب پاسخ دهید. از ویژگیهای این کتابخانه امکان اجرای آن بدون نیاز به وجود وب سرور و به صورت Standalone میباشد.
بهتر است وارد عمل شویم و ببینیم این سیستم چگونه عمل میکند. برای شروع ما خود Asp .net را به عنوان میزبان در نظر میگیریم.
- یک پروژه خالی Asp .net ایجاد کنید. (Asp .net Empty Web Application)
- وارد خط فرمان Package Manager شوید و عبارت زیر را وارد کنید:
یک کلاس به پروژه اضافه کنید و نام آن را TestModule بگذارید.
در متن کلاس عبارات زیر را تایپ کنید:
حال کلید F5 را زده و برنامه را اجرا کنید.
حالا در مرورگر خودتان عبارت http://localhost:12345/Hello را تایپ کنید. توجه کنید که به جای 12345 باید شماره پورتی که وب سرور دات نت اجراست تایپ کنید.
همانطور که متوجه شدید ما در خطوط بالا تعیین کرده ایم که برای درخواستهای از نوع Get که مسیر ریشه سایت را درخواست میکنند عبارت Welcome to my site! ارسال شود. همچنین برای درخواست هایی که مسیر /Hello را درخواست میکنند عبارت Hello Nancy نمایان میشود.
نکته جالب برای درخواستهای /Bye است. در اینجا ما یک پارامتر به نام name تعریف کرده ایم و گفته ایم که درخواستهای Bye که در ادامه آنها عبارتی وجود دارد به صورت Good bye به همراه عبارت بازگردانده شوند.
همین عملیات برای درخواستهای Put و Head و سایر انواع درخواست وجود دارد.
برای اینکه درخواستها از مسیر خاصی فراخوانی شوند کافی است در هنگام اعلان سازنده کلاس ماژول، مسیر را تعیین کنید.
این درخواستها از مسیری شبیه این مسیر فراخوانی خواهند شد:
همچنین امکان بازگرداندن کدهای وضعیت به طور مستقیم وجود دارد.
و یا
همانطور که گفته شد امکان میزبانی پروژههای Nancy از داخل برنامههای دات نتی هم وجود دارد. در مقاله بعدی به این موضوع خواهیم پرداخت.
بهتر است وارد عمل شویم و ببینیم این سیستم چگونه عمل میکند. برای شروع ما خود Asp .net را به عنوان میزبان در نظر میگیریم.
- یک پروژه خالی Asp .net ایجاد کنید. (Asp .net Empty Web Application)
- وارد خط فرمان Package Manager شوید و عبارت زیر را وارد کنید:
Install-Package Nancy.Hosting.Aspnet
- پروژههای Nancy از تعدای ماژول تشکیل شده اند که میتوانیم برای هر ماژول آدرس وب جداگانه ای در نظر بگیریم. یک کلاس به پروژه اضافه کنید و نام آن را TestModule بگذارید.
در متن کلاس عبارات زیر را تایپ کنید:
public class TestModule : NancyModule { public TestModue() { Get["/"] = x => "Welcome to my site!"; Get["/Hello/"] = x=> "Hello Nancy!"; Get["/Bye/{name}"] = x=> "Good bye " + x.name; } }
حال کلید F5 را زده و برنامه را اجرا کنید.
حالا در مرورگر خودتان عبارت http://localhost:12345/Hello را تایپ کنید. توجه کنید که به جای 12345 باید شماره پورتی که وب سرور دات نت اجراست تایپ کنید.
همانطور که متوجه شدید ما در خطوط بالا تعیین کرده ایم که برای درخواستهای از نوع Get که مسیر ریشه سایت را درخواست میکنند عبارت Welcome to my site! ارسال شود. همچنین برای درخواست هایی که مسیر /Hello را درخواست میکنند عبارت Hello Nancy نمایان میشود.
نکته جالب برای درخواستهای /Bye است. در اینجا ما یک پارامتر به نام name تعریف کرده ایم و گفته ایم که درخواستهای Bye که در ادامه آنها عبارتی وجود دارد به صورت Good bye به همراه عبارت بازگردانده شوند.
همین عملیات برای درخواستهای Put و Head و سایر انواع درخواست وجود دارد.
برای اینکه درخواستها از مسیر خاصی فراخوانی شوند کافی است در هنگام اعلان سازنده کلاس ماژول، مسیر را تعیین کنید.
public class TestModule : NancyModule { public TestModule() : base("/test") { Get["/user"] = x=> "It is test for user"; } }
این درخواستها از مسیری شبیه این مسیر فراخوانی خواهند شد:
http://localhost:12345/test/user
همانطور که برای پردازش درخواست از عبارات کوتاه استفاده کردیم میتوانیم از توابع هم استفاده کنیم.Get["/hello/{username}"] = x=> { // some code // ... // ... return "Hello, " + username; };
همچنین امکان بازگرداندن کدهای وضعیت به طور مستقیم وجود دارد.
Get["/user"] = x=> return 200;
و یا
Get["/user] = x=> return HttpStatusCode.OK;
همانطور که گفته شد امکان میزبانی پروژههای Nancy از داخل برنامههای دات نتی هم وجود دارد. در مقاله بعدی به این موضوع خواهیم پرداخت.
روش متداول کار با تزریق وابستگیهای برنامههای مبتنی بر NET Core.، عموما با ثبت و معرفی یک سرویس به صورت زیر، توسط متدهای AddTransient، AddSingleton و AddScoped است:
و سپس استفادهی از این سرویس، با تزریق آن در سازندهی یک کنترلر که نمونههای بیشتری از آنرا در قسمت چهارم بررسی کردیم:
در اینجا کار وهله سازی DefaultCustomerService به صورت خودکار و راسا توسط IoC Container توکار برنامه صورت میگیرد و ما هیچگونه دخالتی را در آن نداریم. اما اگر در این بین نیاز باشد پس از وهله سازی DefaultCustomerService، یک خاصیت آن نیز بر اساس شرایط جاری مقدار دهی شود و حاصل نهایی در اختیار SupportController فوق قرار گیرد چه باید کرد؟
برای سفارشی سازی مراحل وهله سازی اشیاء توسط IoC Container توکار برنامه و امکان دخالت در آن، قابلیتی تحت عنوان «factory registration» نیز پیش بینی شدهاست که در ادامه آنرا بررسی میکنیم.
Factory Registration چیست؟
اگر در اسمبلی Microsoft.Extensions.DependencyInjection.Abstractions و فضای نام Microsoft.Extensions.DependencyInjection آن به کلاس ServiceCollectionServiceExtensions که متدهای الحاقی مانند AddScoped را ارائه میکند، بیشتر دقت کنیم، تک تک این متدها امضاهای دیگری را نیز دارند:
همانطور که ملاحظه میکنید، امضای تعدادی از این overloadها، دارای پارامترهایی از نوع Func نیز هست و هدف آنها فراهم آوردن روشی برای سفارشی سازی مراحل وهله سازی سرویسیهای بازگشتی از طریق سیستم تزریق وابستگیهای برنامه است. توسط این پارامتر، پیش از وهله سازی سرویس درخواستی، IServiceProvider جاری یا همان root container را در اختیار شما قرار میدهد (اطلاعات بیشتر در مورد IServiceProvider را در قسمت دوم بررسی کردیم) و توسط آن میتوان ابتدا وهلهای از سرویس یا سرویسهای خاصی را دریافت کرد و پس از ترکیب و سفارشی سازی آنها، در آخر یک object را بازگشت داد که در نهایت به عنوان وهلهی اصلی این سرویس درخواستی، در سراسر برنامه مورد استفاده قرار میگیرد. در ادامه با مثالهایی، کاربردهای این پارامتر از نوع Func، یا Implementation Factory را بررسی میکنیم.
مثال 1 : تزریق وابستگیها در حالتیکه کلاس سرویس مدنظر دارای تعدادی پارامتر ثابت است
IoC Container توکار برنامههای NET Core.، به صورت خودکار وابستگیهای تزریق شدهی در سازندههای سرویسهای مختلف را تا هر چند سطح ممکن، به صورت خودکار وهله سازی میکند؛ به شرطیکه این وابستگیهای تزریق شده نیز خودشان سرویس بوده باشند و در تنظیمات ابتدایی آن ثبت و معرفی شده باشند. به عبارتی زمانیکه با سیستم تزریق وابستگیها کار میکنیم، مهم نیست که نگران مقدار دهی پارامترهای سازندهی تزریق شدهی در سازندههای سرویسی خاص باشیم. اما ... برای نمونه سرویس زیر را که یک رشته را در سازندهی خود دریافت میکند درنظر بگیرید:
اینبار دیگر نمیتوان این سرویس را از طریق متداول زیر ثبت و معرفی کرد:
چون IoC Container نمیداند که چگونه و از کجا باید پارامتر رشتهای درخواستی در سازندهی کلاس ParameterizedService را تامین کند. همچنین ثبت سرویسها نیز در کلاس ServiceCollectionServiceExtensions معرفی شدهی در ابتدای بحث، به قید «where TService : class» محدود شدهاست. اینجا است که روش factory registration به کمک ما خواهد آمد تا بتوانیم مراحل وهله سازی این سرویس را سفارشی سازی کنیم:
البته چون بدنهی این Func، صرفا از یک return تشکیل شدهاست، معادل ساده شدهی زیر را هم میتواند داشته باشد:
اینبار در سراسر برنامه اگر سرویس IParameterizedService درخواست شود، وهلهای از کلاس ParameterizedService را با پارامتر سازندهی "some value ...."، دریافت خواهد کرد.
در اینجا چون serviceProvider نیز در اختیار ما است، حتی میتوان این مقدار را از سرویسی دیگر دریافت کرد و سپس مورد استفاده قرار داد:
نمونهی دیگری از این دست، کار با IUrlHelper توکار ASP.NET Core است. این سرویس برای اینکه پاسخ درستی را ارائه دهد، نیاز به ActionContext جاری را دارد تا بتواند از طریق آن به تمام جزئیات اکشن متد یک کنترلر و درخواست رسیده دسترسی داشته باشد. در این حالت برای ساده سازی کار با آن، بهتر است تامین وابستگیهای لحظهای این سرویس را با سفارشی سازی نحوهی وهله سازی آن، انجام دهیم، تا اینکه این قطعه کد تکراری را در هر جائیکه به IUrlHelper نیاز است، تکرار کنیم:
اکنون اگر IUrlHelper را به سازندهی یک کنترلر تزریق کنیم، دیگر نیازی به سه سطر نوشتهی تامین factory و action context آن نخواهد بود.
مثال 2: وهله سازی در صورت نیاز به وابستگیهای یک سرویس، به کمک Lazy loading
فرض کنید دو سرویس را در سازندهی سرویس دیگری تزریق کردهاید:
بعد در این کلاس، در یک متد، از سرویس accounting استفاده میشود و در متدی دیگر از سرویس sales. یعنی هرچند در زمان وهله سازی شیء OrderHandler هر دو وابستگی تزریق شدهی در سازندهی آن نیز وهله سازی خواهند شد، اما در بسیاری از شرایط، بسته به متد مورد استفاده، فقط از یکی از آنها استفاده میکنیم. اکنون این سؤال مطرح میشود که آیا میتوان سربار وهله سازی تمام سازندههای این کلاس را به زمان استفادهی از آنها منتقل کرد؟ یعنی سرویس accounting تزریق شده فقط زمانی وهله سازی شود که واقعا قرار است از آن استفاده کنیم.
روش انجام یک چنین کارهایی با استفاده از کلاس Lazy اضافه شدهی به NET 4x. قابل انجام است:
و برای معرفی آن در اینجا میتوان از روش factory registration استفاده کرد:
- در اینجا در ابتدا تمام سرویسها (حتی آنهایی که قرار است به صورت Lazy استفاده شوند) یکبار به صورت متداولی معرفی میشوند.
- سپس سرویسهایی که قرار است به صورت Lazy نیز واکشی شوند، بار دیگر توسط روش factory registration با وهله سازی new Lazy از نوع سرویس مدنظر و فراهم آوردن پیاده سازی آن با استفاده از serviceProvider.GetRequiredService، مجددا معرفی خواهند شد.
پس از این تنظیمات، اگر سرویس IOrderHandler را از طریق سیستم تزریق وابستگیها درخواست کنید، وابستگیهای تزریق شدهی در سازندهی آن فقط زمانی و در محلی وهله سازی میشوند که از طریق خاصیت Value شیء Lazy آنها مورد استفاده قرار گرفته شده باشند.
مثال کامل IOrderHandler را از فایل پیوستی انتهای مطلب میتوانید دریافت. اگر آنرا اجرا کنید (برنامهی کنسول آنرا)، در خروجی آن، فقط اجرا شدن سازندهی سرویسی را مشاهده میکنید که مورد استفاده قرار گرفته و نه وابستگی دومی که تزریق شده، اما استفاده نشدهاست.
مثال 3: چگونه بجای اینترفیسها، یک وهله از کلاسی مشخص را از سیستم تزریق وابستگیها درخواست کنیم؟
فرض کنید سرویسی را به صورت زیر به سیستم تزریق وابستگیها معرفی کردهاید:
در ادامه اگر سرویس IMyDisposableService را از این سیستم درخواست کنیم، برنامه بدون مشکل اجرا میشود؛ اما اگر خود MyDisposableService را تزریق کنیم چطور؟
در این حالت برنامه با استثنای زیر متوقف میشود و عنوان میکند که نمیداند چگونه باید این وابستگی تزریق شده را تامین کند:
این مورد را نیز میتوان توسط factory registration به نحو زیر مدیریت کرد:
هر زمانیکه وهلهای از کلاس MyDisposableService درخواست شود، وهلهای از سرویس IMyDisposableService را بازگشت میدهیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: CoreDependencyInjectionSamples-06.zip
public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddTransient<ICustomerService, DefaultCustomerService>(); // ... } }
public class SupportController { // DefaultCustomerService will be injected here: public SupportController(ICustomerService customerService) { // ... } }
برای سفارشی سازی مراحل وهله سازی اشیاء توسط IoC Container توکار برنامه و امکان دخالت در آن، قابلیتی تحت عنوان «factory registration» نیز پیش بینی شدهاست که در ادامه آنرا بررسی میکنیم.
Factory Registration چیست؟
اگر در اسمبلی Microsoft.Extensions.DependencyInjection.Abstractions و فضای نام Microsoft.Extensions.DependencyInjection آن به کلاس ServiceCollectionServiceExtensions که متدهای الحاقی مانند AddScoped را ارائه میکند، بیشتر دقت کنیم، تک تک این متدها امضاهای دیگری را نیز دارند:
namespace Microsoft.Extensions.DependencyInjection { public static class ServiceCollectionServiceExtensions { public static IServiceCollection AddScoped<TService>( this IServiceCollection services) where TService : class; public static IServiceCollection AddScoped( this IServiceCollection services, Type serviceType, Type implementationType); public static IServiceCollection AddScoped( this IServiceCollection services, Type serviceType, Func<IServiceProvider, object> implementationFactory); public static IServiceCollection AddScoped<TService, TImplementation>(this IServiceCollection services) public static IServiceCollection AddScoped( this IServiceCollection services, Type serviceType); public static IServiceCollection AddScoped<TService>( this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class; public static IServiceCollection AddScoped<TService, TImplementation>( this IServiceCollection services, Func<IServiceProvider, TImplementation> implementationFactory) // ... } }
مثال 1 : تزریق وابستگیها در حالتیکه کلاس سرویس مدنظر دارای تعدادی پارامتر ثابت است
IoC Container توکار برنامههای NET Core.، به صورت خودکار وابستگیهای تزریق شدهی در سازندههای سرویسهای مختلف را تا هر چند سطح ممکن، به صورت خودکار وهله سازی میکند؛ به شرطیکه این وابستگیهای تزریق شده نیز خودشان سرویس بوده باشند و در تنظیمات ابتدایی آن ثبت و معرفی شده باشند. به عبارتی زمانیکه با سیستم تزریق وابستگیها کار میکنیم، مهم نیست که نگران مقدار دهی پارامترهای سازندهی تزریق شدهی در سازندههای سرویسی خاص باشیم. اما ... برای نمونه سرویس زیر را که یک رشته را در سازندهی خود دریافت میکند درنظر بگیرید:
namespace CoreIocServices { public interface IParameterizedService { string GetConstructorParameter(); } public class ParameterizedService : IParameterizedService { private readonly string _connectionString; public ParameterizedService(string connectionString) { _connectionString = connectionString; } public string GetConstructorParameter() { return _connectionString; } } }
services.AddTransient<IParameterizedService, ParameterizedService>();
services.AddTransient<IParameterizedService>(serviceProvider => { return new ParameterizedService("some value ...."); });
services.AddTransient<IParameterizedService>(serviceProvider => new ParameterizedService("some value ...."));
در اینجا چون serviceProvider نیز در اختیار ما است، حتی میتوان این مقدار را از سرویسی دیگر دریافت کرد و سپس مورد استفاده قرار داد:
services.AddTransient<IParameterizedService>(serviceProvider => { var config = serviceProvider.GetRequiredService<ITestService>().GetConfigValue(); return new ParameterizedService(config); });
نمونهی دیگری از این دست، کار با IUrlHelper توکار ASP.NET Core است. این سرویس برای اینکه پاسخ درستی را ارائه دهد، نیاز به ActionContext جاری را دارد تا بتواند از طریق آن به تمام جزئیات اکشن متد یک کنترلر و درخواست رسیده دسترسی داشته باشد. در این حالت برای ساده سازی کار با آن، بهتر است تامین وابستگیهای لحظهای این سرویس را با سفارشی سازی نحوهی وهله سازی آن، انجام دهیم، تا اینکه این قطعه کد تکراری را در هر جائیکه به IUrlHelper نیاز است، تکرار کنیم:
services.AddScoped<IUrlHelper>(serviceProvider => { var actionContext = serviceProvider.GetRequiredService<IActionContextAccessor>().ActionContext; var urlHelperFactory = serviceProvider.GetRequiredService<IUrlHelperFactory>(); return urlHelperFactory.GetUrlHelper(actionContext); });
مثال 2: وهله سازی در صورت نیاز به وابستگیهای یک سرویس، به کمک Lazy loading
فرض کنید دو سرویس را در سازندهی سرویس دیگری تزریق کردهاید:
namespace Services { public class OrderHandler : IOrderHandler { private readonly IAccounting _accounting; private readonly ISales _sales; public OrderHandler(IAccounting accounting, ISales sales) {
روش انجام یک چنین کارهایی با استفاده از کلاس Lazy اضافه شدهی به NET 4x. قابل انجام است:
public class OrderHandlerLazy : IOrderHandler { public OrderHandlerLazy(Lazy<IAccounting> accounting, Lazy<ISales> sales) {
services.AddTransient<IOrderHandler, OrderHandlerLazy>(); services.AddTransient<IAccounting, Accounting>() .AddTransient(serviceProvider => new Lazy<IAccounting>(() => serviceProvider.GetRequiredService<IAccounting>())); services.AddTransient<ISales, Sales>() .AddTransient(serviceProvider => new Lazy<ISales>(() => serviceProvider.GetRequiredService<ISales>()));
- سپس سرویسهایی که قرار است به صورت Lazy نیز واکشی شوند، بار دیگر توسط روش factory registration با وهله سازی new Lazy از نوع سرویس مدنظر و فراهم آوردن پیاده سازی آن با استفاده از serviceProvider.GetRequiredService، مجددا معرفی خواهند شد.
پس از این تنظیمات، اگر سرویس IOrderHandler را از طریق سیستم تزریق وابستگیها درخواست کنید، وابستگیهای تزریق شدهی در سازندهی آن فقط زمانی و در محلی وهله سازی میشوند که از طریق خاصیت Value شیء Lazy آنها مورد استفاده قرار گرفته شده باشند.
مثال کامل IOrderHandler را از فایل پیوستی انتهای مطلب میتوانید دریافت. اگر آنرا اجرا کنید (برنامهی کنسول آنرا)، در خروجی آن، فقط اجرا شدن سازندهی سرویسی را مشاهده میکنید که مورد استفاده قرار گرفته و نه وابستگی دومی که تزریق شده، اما استفاده نشدهاست.
مثال 3: چگونه بجای اینترفیسها، یک وهله از کلاسی مشخص را از سیستم تزریق وابستگیها درخواست کنیم؟
فرض کنید سرویسی را به صورت زیر به سیستم تزریق وابستگیها معرفی کردهاید:
services.AddTransient<IMyDisposableService, MyDisposableService>();
public class AnotherController { public AnotherController(MyDisposableService customerService) { // ... } }
An unhandled exception occurred while processing the request. InvalidOperationException: Unable to resolve service for type ‘MyDisposableService’ while attempting to activate ‘AnotherController’. Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, bool isDefaultParameterRequired)
services.AddTransient<IMyDisposableService, MyDisposableService>(); services.AddTransient<MyDisposableService>(serviceProvider => serviceProvider.GetRequiredService<IMyDisposableService>() as MyDisposableService);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: CoreDependencyInjectionSamples-06.zip
نظرات مطالب
تقویم شمسی در ویندوز 10
یک نکتهی تکمیلی
در ویندوز 10 که تقویم شمسی آن انتخاب شده، فرض کنید قصد داریم رشتهی زیر را به تاریخ معادل آن تبدیل کنیم:
این کد با خطای زیر که بیانگر قابل تفسیر نبودن روز Wed است خاتمه مییابد:
راه حل:
شبیه به مطلب «تاثیر فرهنگ جاری سیستم بر روی اعداد در دات نت» در اینجا نیز باید از CultureInfo.InvariantCulture استفاده کرد:
در ویندوز 10 که تقویم شمسی آن انتخاب شده، فرض کنید قصد داریم رشتهی زیر را به تاریخ معادل آن تبدیل کنیم:
const string dateTime = "Wed, 17 Apr 2013 12:10:18 GMT"; Console.WriteLine(DateTime.Parse(dateTime));
String was not recognized as a valid DateTime because the day of week was incorrect.
شبیه به مطلب «تاثیر فرهنگ جاری سیستم بر روی اعداد در دات نت» در اینجا نیز باید از CultureInfo.InvariantCulture استفاده کرد:
Console.WriteLine(DateTime.Parse(dateTime, CultureInfo.InvariantCulture));