بهبود کارآیی برنامههای ASP.NET Core 2.1 هاست شدهی در IIS
تا پیش از نگارش 2.1، تمام درخواستهای رسیدهی به IIS توسط یک پروکسی معکوس به نام ASP.NET Core Module به Kestrel ارسال میشدند:
در نگارش 2.1، میتوان کل برنامه را داخل IIS worker process یا همان w3wp.exe هاست کرد تا تاثیر منفی این پروکسی معکوس حذف شود. با بررسیهای انجام شده، این روش بهبود کارآیی بیش از 4 برابری را به همراه دارد؛ نسبت به حالت out-of-process فعلی (تصویر فوق).
برای فعالسازی آن یا فایل web.config را به صورت ذیل جهت تعیین hostingModel ویرایش کنید:
و یا فایل csproj برنامه را به صورت زیر تنظیم نمائید:
تا پیش از نگارش 2.1، تمام درخواستهای رسیدهی به IIS توسط یک پروکسی معکوس به نام ASP.NET Core Module به Kestrel ارسال میشدند:
در نگارش 2.1، میتوان کل برنامه را داخل IIS worker process یا همان w3wp.exe هاست کرد تا تاثیر منفی این پروکسی معکوس حذف شود. با بررسیهای انجام شده، این روش بهبود کارآیی بیش از 4 برابری را به همراه دارد؛ نسبت به حالت out-of-process فعلی (تصویر فوق).
برای فعالسازی آن یا فایل web.config را به صورت ذیل جهت تعیین hostingModel ویرایش کنید:
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" hostingModel="inprocess" />
<PropertyGroup> <AspNetCoreModuleHostingModel>inprocess</AspNetCoreModuleHostingModel> </PropertyGroup>
اشتراکها
معرفی Ignite.NET
در مطالب قبلی (1 , 2) الگوی CQRS معرفی شد. همانطور که میبینید، پیاده سازی این الگو هرچند با فریمورک آمادهای همچون SimpleCQRS، دارای پیچیدگی زیادی است و باعث نوشتن حجم زیادی کد میشود.
فریمورک MediatR توسط توسعه دهنده کتابخانهی محبوب AutoMapper ایجاد شدهاست. این فریمورک پیاده سازی کاملی از الگوی طراحی Mediator در NET. است که داخل خود، تمام پیچیدگیهای پیاده سازی CQRS را Abstract کرده و با حداقل کد ممکن، میتوانید بهراحتی CQRS را داخل پروژهی خود پیاده سازی کنید.
در این سری مطالب به بررسی کامل الگوی CQRS و مزایا و معایب استفاده از آن میپردازیم و سپس با استفاده از کتابخانهی Mediatr، این الگو را داخل یک پروژه پیاده سازی میکنیم.
CQRS
در CQRS متدهای برنامه به 2 بخش Read و Write تقسیم میشوند. بخشهایی که State کلی برنامه ( شامل Database, Cookie, Session, LocalStorage, Memory و ... ) را تغییر میدهند، Command و بخشهایی که صرفا جنبه خواندنی دارند و وضعیت سیستم را تغییر نمیدهند مثل خواندن و نشان دادن اطلاعات از دیتابیس، Query مینامند.
* نکته : Naming Convention مورد استفاده برای Commandها به صورت دستوری است و کار Command در نام آن مشخص است؛ مثال : RegisterUser, SendForgottenPasswordEmail, PlaceOrder
* نکته : Naming Convention مورد استفاده برای Commandها به صورت دستوری است و کار Command در نام آن مشخص است؛ مثال : RegisterUser, SendForgottenPasswordEmail, PlaceOrder
مزایا:
1- شما میتوانید تکنولوژیهای مورد استفادهی در بخشهای Command و Query برنامهی خود را بهراحتی از هم جدا سازید. بهعنوان مثال Apache Cassandra در ذخیره سازی دادهها ( Write Side ) به عنوان یک دیتابیس قابل اعتنا شناخته میشود و از طرفی دیگر ElasticSearch بدلیل سرعت فوق العادهی خود، برای خواندن دادهها استفاده میشود. در این روش، دیتابیسها باید Sync باشند تا دادههای بهروز به کاربر نمایش داده شود که این موضوع چالشهای خود همچون Eventual Consistency و Strong Consistency را دارد که در مقالات بعدی آنها را بررسی خواهیم کرد.
2- در برنامههای معمول، اکثرا بخش Read Side، بیشتر از Write Side استفاده میشود و کاربران معمولا اطلاعات را دریافت و میبینند تا اینکه در آن تغییری ایجاد کنند؛ در این صورت شما میتوانید بخش Read برنامهی خود را Scale کرده و تعداد سیستم یا منابع بیشتری را به این قسمت از برنامهی خود اختصاص دهید ( Horizontal Scaling, Vertical Scaling ).
3- این جداسازی باعث تمرکز بیشتر شما بر روی قسمتهای مختلف برنامه میشود؛ بخشهایی که وضعیت سیستم را تغییر میدهند از بخشهایی که صرفا دادههایی را خوانده و نمایش میدهند، بطور کامل جدا شدهاند و بهراحتی قابلیت تغییر هرکدام از این بخشها را خواهید داشت.
3- این جداسازی باعث تمرکز بیشتر شما بر روی قسمتهای مختلف برنامه میشود؛ بخشهایی که وضعیت سیستم را تغییر میدهند از بخشهایی که صرفا دادههایی را خوانده و نمایش میدهند، بطور کامل جدا شدهاند و بهراحتی قابلیت تغییر هرکدام از این بخشها را خواهید داشت.
معایب : معمولا از معایب این الگو، از پیچیدگی پیاده سازی آن یاد میشود که در این آموزش با استفاده از Mediatr سعی بر از بین بردن این پیچیدگی را داریم.
Events
Eventها رویدادهایی هستند که خبر انجام کاری را که قبلا داخل سیستم انجامش به پایان رسیده است، به Consumerهای خود میدهند. بعنوان مثال میخواهیم بعد از ثبت نام موفق یک کاربر داخل سیستم، Notification و یا ایمیلی را به او ارسال کنیم. بعد از ثبت نام کاربر میتوانیم Event ای به نام UserRegistered را که شامل Username و Email کاربر در بدنه خود است، Raise کنیم.
Eventها میتوانند چندین Consumer داشته باشند؛ بنابراین میتوانیم یک EventHandler را برای UserRegistered بنویسیم که Email ارسال کند و EventHandler دیگری ایجاد کنیم که Notification ای را برای کاربر بفرستد.
Eventها میتوانند چندین Consumer داشته باشند؛ بنابراین میتوانیم یک EventHandler را برای UserRegistered بنویسیم که Email ارسال کند و EventHandler دیگری ایجاد کنیم که Notification ای را برای کاربر بفرستد.
* نکته : Naming Convention مورد استفاده برای Eventها به صورت گذشتهاست و خبر یک کار، که قبلا انجام شده است را میدهد؛ مثال : UserRegistered, OrderPlaced
Event Sourcing
Event Sourcing به معنای ذخیرهی تمام Eventهای رخ داده در برنامه داخل یک دیتابیس Append-Only است. در این نوع دیتابیسها فقط میتوانیم Eventهای جدیدی به آن اضافه کنیم و قادر به ویرایش و حذف Eventها نیستیم؛ چون منطق Event، کارهایی است که در گذشته اتفاق افتادهاند و ما قادر به تغییر چیزی که در گذشته رخ دادهاست، نیستیم.
مزیت Event Sourcing این است که State برنامه را در زمانهای مختلفی نگه داشتهایم و میتوانیم وضعیت سیستم را در تاریخی مشخص، پیدا کنیم و در صورت بهوجود آمدن مشکلی در سیستم، وضعیت آن را تا قبل از به مشکل خوردن، بررسی کنیم.
بعنوان مثال مبلغ یک حساب بانکی را در نظر بگیرید. یکی از راههای بهروز نگه داشتن این مبلغ بعد از هر تراکنش، در نظر گرفتن یک فیلد برای مبلغ و انجام عمل Update بعد از هر تراکنش بطور مستقیم برروی آن است. در این روش بهدلیل آپدیت کردن مستقیم این فیلد داخل دیتابیس، ما وضعیت قبلی (مبلغ قبلی) را از دست خواهیم داد و برای رسیدن به مبلغ قبلی مجبور به زدن چندین کوئری دیتابیسی و دریافت تراکنشهای قبلی و ... برای رسیدن به وضعیت قبلی سیستم هستیم.
روش دیگری وجود دارد که بجای بهروزرسانی مداوم state جاری، تمام Event هایی که در آن تراکنشی داخل سیستم رخ داده و این تراکنش State برنامه را تحت تاثیر خود قرار دادهاست، داخل یک دیتابیس اضافه نماییم. در این صورت بدلیل داشتن تمام رویدادهای اتفاق افتادهی در برنامه، میتوان وضعیت جاری سیستم را شبیه سازی و متوجه شد.
* در این سری آموزشی از دیتابیس Event Store برای پیاده سازی Event Sourcing استفاده خواهیم کرد.
مزیت Event Sourcing این است که State برنامه را در زمانهای مختلفی نگه داشتهایم و میتوانیم وضعیت سیستم را در تاریخی مشخص، پیدا کنیم و در صورت بهوجود آمدن مشکلی در سیستم، وضعیت آن را تا قبل از به مشکل خوردن، بررسی کنیم.
بعنوان مثال مبلغ یک حساب بانکی را در نظر بگیرید. یکی از راههای بهروز نگه داشتن این مبلغ بعد از هر تراکنش، در نظر گرفتن یک فیلد برای مبلغ و انجام عمل Update بعد از هر تراکنش بطور مستقیم برروی آن است. در این روش بهدلیل آپدیت کردن مستقیم این فیلد داخل دیتابیس، ما وضعیت قبلی (مبلغ قبلی) را از دست خواهیم داد و برای رسیدن به مبلغ قبلی مجبور به زدن چندین کوئری دیتابیسی و دریافت تراکنشهای قبلی و ... برای رسیدن به وضعیت قبلی سیستم هستیم.
روش دیگری وجود دارد که بجای بهروزرسانی مداوم state جاری، تمام Event هایی که در آن تراکنشی داخل سیستم رخ داده و این تراکنش State برنامه را تحت تاثیر خود قرار دادهاست، داخل یک دیتابیس اضافه نماییم. در این صورت بدلیل داشتن تمام رویدادهای اتفاق افتادهی در برنامه، میتوان وضعیت جاری سیستم را شبیه سازی و متوجه شد.
* در این سری آموزشی از دیتابیس Event Store برای پیاده سازی Event Sourcing استفاده خواهیم کرد.
در مقالهی بعدی، امکانات فریمورک MediatR را بررسی خواهیم کرد.
در ادامهی سری «ارتقاء به ASP.NET Core 1.0» اگر بخواهیم مباحث اعتبارسنجی کاربران و ASP.NET Identity مخصوص آنرا بررسی کنیم، نیاز است ابتدا مباحث Entity framework Core 1.0 را بررسی کنیم. به همین جهت در طی چند قسمت مباحث پایهای کار با EF Core 1.0 را در ASP.NET Core 1.0، بررسی خواهیم کرد. بنابراین پیشنیاز ضروری این مباحث، مطالعهی سری «ارتقاء به ASP.NET Core 1.0» است و در آن از مباحثی مانند چگونگی کار با فایلهای کانفیگ جدید، تزریق وابستگیها و سرویسها، فعال سازی سرویس Logging، فعال سازی صفحات مخصوص توسعه دهندهها و ... در ASP.NET Core 1.0 استفاده خواهد شد.
EF Core چیست؟
EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانکهای اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر میکند. توسط آن قسمت عمدهی کدهای مستقیم کار با بانکهای اطلاعاتی حذف شده و تبدیل به کدهای دات نتی میشوند. مزیت این لایهی Abstraction اضافی (لایهای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامهاست؛ بدون نیاز به تغییری در سایر قسمتهای برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر میکنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر میرسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئریها، رفتار بسیاری از بانکهای اطلاعاتی با آنها همانند رویههای ذخیره شده است که به عدم تولید Query planهای مجزایی به ازای هر کوئری رسیده منجر میشود که در نهایت سبب بالا رفتن سرعت اجرای کوئریها و مصرف حافظهی کمتری در سمت سرور بانک اطلاعاتی میگردد.
تفاوت EF Core با نگارشهای دیگر Entity framework در چیست؟
سورس باز بودن
EF از نگارشهای آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارشهای پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.
چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفادهی از آن در انواع و اقسام برنامههای دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامههای مختلف به شرح زیر است:
افزایش تعداد بانکهای اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانکهای اطلاعاتی رابطهای بود و همچنین مایکروسافت صرفا نگارشهای مختلف SQL Server را به صورت رسمی پشتیبانی میکرد و برای سایر بانکهای اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانکهای اطلاعاتی رابطهای، پشتیبانی از بانکهای اطلاعاتی NoSQL هم اضافه شدهاست؛ به همراه پروایدر ویژهای به نام In Memory جهت انجام سادهتر Unit testing. کاری که با نگارشهای پیشین EF به سادگی و از روز اول پشتیبانی نمیشد.
حذف و یا عدم پیاده سازی تعدادی از قابلیتهای EF 6.x
اگر موارد فوق جزو مهمترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگیها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامههای UWP نیاز دارد تمام نوعها را همانند زبانهای C و ++C، در زمان کامپایل بداند)، یک سری از قابلیتهای EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمیشوند. لیست کامل و به روز شدهی این موارد را در اینجا میتوانید مطالعه کنید.
بنابراین امکان انتقال برنامههای EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.
برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0
برای نصب تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بستهی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
البته در این قسمت صرفا از بستهی اول که جهت اتصال به SQL Server است استفاده میکنیم. بستههای دیگر را در قسمتهای بعد، برای به روز رسانی اسکیمای بانک اطلاعاتی (مباحث Migrations) و مباحث scaffolding استفاده خواهیم کرد.
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
این مداخلی که توسط نیوگت اضافه شدهاند، نیاز به اصلاح دارند؛ به این صورت:
نیاز است در قسمت dependencies مشخص کنیم که ابزارهای اضافه شده مخصوص build هستند و نه اجرای برنامه. همچنین قسمت tools را باید با Microsoft.EntityFrameworkCore.Tools مقدار دهی کرد تا بتوان از این ابزار در خط فرمان، جهت اجرای فرامین migrations استفاده کرد.
بنابراین از همین ابتدای کار، بدون مراجعهی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آنرا ذخیره کنید؛ تا کار به روز رسانی و نصب بستهها، به صورت خودکار و همچنین صحیحی انجام شود.
فعال سازی صفحات مخصوص توسعه دهندههای EF Core 1.0
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهندهها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بستهی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحهی نمایش خطاهای بانک اطلاعاتی را میسر میکند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
سپس میتوان متد جدید UseDatabaseErrorPage را در متد Configure کلاس آغازین برنامه، فراخوانی کرد:
با فعال سازی این صفحه، اگر در حین توسعهی برنامه و اتصال به بانک اطلاعاتی، خطایی رخ دهد، بجای مشاهدهی یک صفحهی خطای عمومی (اگر UseDeveloperExceptionPage را فعال کرده باشید)، اینبار ریز جزئیات بیشتری را به همراه توصیههایی از طرف تیم EF مشاهده خواهید کرد.
تعریف اولین Context برنامه و مشخص سازی رشتهی اتصالی آن
در این تصویر، زیر ساخت نگاشتهای EF Core را مشاهده میکنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرندهی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل اینها را مشاهده میکنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که اینها نیز به فیلدها و ستونهای آن جدول در سمت بانک اطلاعاتی نگاشت میشوند.
بنابراین برای شروع کار، پوشهای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه میکنیم:
کلاس Person بیانگر ساختار جدول اشخاص بانک اطلاعاتی است. برای اینکه این کلاس را تبدیل و نگاشت به یک جدول کنیم، نیاز است آنرا به صورت یک DbSet در معرض دید EF Core قرار دهیم و اینکار در کلاسی که از DbContex مشتق میشود، صورت خواهد گرفت:
بنابراین در ادامه کلاس جدید ApplicationDbContext را که از کلاس پایه DbContext مشتق میشود تعریف کرده و سپس کلاس Person را به صورت یک DbSet در معرض دید EF Core قرار میدهیم.
سازندهی این کلاس نیز به نحو خاصی تعریف شدهاست. اگر به سورسهای EF Core مراجعه کنیم، کلاس پایهی DbContext دارای دو سازندهی با و بدون پارامتر است:
اگر از سازندهی بدون پارامتر استفاده کنیم و برای مثال در کلاس ApplicationDbContext فوق، به طور کامل سازندهی تعریف شده را حذف کنیم، باید به نحو ذیل تنظیمات بانک اطلاعاتی را مشخص کنیم:
در این حالت باید متد OnConfiguring را override و یا بازنویسی کنیم، تا بتوان از اول مشخص کرد که قرار است از پروایدر SQL Server استفاده کنیم و ثانیا رشتهی اتصالی به آن چیست.
اما چون در یک برنامهی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام میشود و در آنجا به سادگی میتوان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشتهی اتصالی را دریافت کرد، مرسوم است از سازندهی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعهی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» به فایل appsettings.json مراجعه کرده و تنظیمات رشتهی اتصالی برنامه را به صورت ذیل در آن مشخص میکنیم:
باید دقت داشت که نام این مداخل کاملا اختیاری هستند و در نهایت باید در کلاس آغازین برنامه به صورت صریحی مشخص شوند.
در اینجا به وهلهی پیش فرض SQL Server اشاره شدهاست؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده میشود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیدهاست.
پس از تعریف رشتهی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازندهی با پارامتر آن استفاده میکنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویسهای ASP.NET Core معرفی میکنیم:
در اینجا جهت یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» نحوهی وهله سازی خاصیت Configuration که در متد UseSqlServer مورد استفاده قرار گرفتهاست، نیز ذکر شدهاست.
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی میکنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.
یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد
با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگیها نیز در آن وجود دارد. یعنی بجای روش فوق، میتوان IConfigurationRoot را به سازندهی کلاس Context برنامه نیز تزریق کرد:
با توجه به اینکه IConfigurationRoot در کلاس ConfigureServices به صورت Singleton، به مجموعهی سرویسهای برنامه معرفی شدهاست، از آن در تمام کلاسهای برنامه که تحت نظر سیستم تزریق وابستگیهای توکار ASP.NET Core هستند، میتوان استفاده کرد.
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا میکند و ساده میشود:
یک نکته: امکان تزریق ApplicationDbContext به تمام کلاسهای برنامه وجود دارد
همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامهی ASP.NET Core نیز وجود دارد:
در اینجا نحوهی تزریق DB Context برنامه را به یک کنترلر مشاهده میکنید. البته هرچند تزریق یک کلاس مشخص به این شکل، تزریق وابستگیها نام ندارد و هنوز این کنترلر دقیقا وابستهاست به پیاده سازی خاص کلاس ApplicationDbContext، اما ... در کل امکان آن هست.
در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:
علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشدهاست و همچنین ساختار جداول را به آن منتقل نکردهایم که این موارد را در قسمتهای بعدی مرور خواهیم کرد.
EF Core چیست؟
EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانکهای اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر میکند. توسط آن قسمت عمدهی کدهای مستقیم کار با بانکهای اطلاعاتی حذف شده و تبدیل به کدهای دات نتی میشوند. مزیت این لایهی Abstraction اضافی (لایهای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامهاست؛ بدون نیاز به تغییری در سایر قسمتهای برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر میکنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر میرسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئریها، رفتار بسیاری از بانکهای اطلاعاتی با آنها همانند رویههای ذخیره شده است که به عدم تولید Query planهای مجزایی به ازای هر کوئری رسیده منجر میشود که در نهایت سبب بالا رفتن سرعت اجرای کوئریها و مصرف حافظهی کمتری در سمت سرور بانک اطلاعاتی میگردد.
تفاوت EF Core با نگارشهای دیگر Entity framework در چیست؟
سورس باز بودن
EF از نگارشهای آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارشهای پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.
چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفادهی از آن در انواع و اقسام برنامههای دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامههای مختلف به شرح زیر است:
• All .NET application (Console, ASP.NET 4, WinForms, WPF) • Mac and Linux applications (Mono) • UWP (Universal Windows Platform) • ASP.NET Core applications • Can use EF Core in Windows phone and Windows store app
افزایش تعداد بانکهای اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانکهای اطلاعاتی رابطهای بود و همچنین مایکروسافت صرفا نگارشهای مختلف SQL Server را به صورت رسمی پشتیبانی میکرد و برای سایر بانکهای اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانکهای اطلاعاتی رابطهای، پشتیبانی از بانکهای اطلاعاتی NoSQL هم اضافه شدهاست؛ به همراه پروایدر ویژهای به نام In Memory جهت انجام سادهتر Unit testing. کاری که با نگارشهای پیشین EF به سادگی و از روز اول پشتیبانی نمیشد.
حذف و یا عدم پیاده سازی تعدادی از قابلیتهای EF 6.x
اگر موارد فوق جزو مهمترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگیها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامههای UWP نیاز دارد تمام نوعها را همانند زبانهای C و ++C، در زمان کامپایل بداند)، یک سری از قابلیتهای EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمیشوند. لیست کامل و به روز شدهی این موارد را در اینجا میتوانید مطالعه کنید.
بنابراین امکان انتقال برنامههای EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.
برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0
برای نصب تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بستهی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final", "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0" } }
{ "dependencies": { // same as before "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "type": "build" }, "Microsoft.EntityFrameworkCore.SqlServer.Design": { "version": "1.0.0", "type": "build" } }, "tools": { // same as before "Microsoft.EntityFrameworkCore.Tools": { "version": "1.0.0-preview2-final", "imports": [ "portable-net45+win8" ] } } }
بنابراین از همین ابتدای کار، بدون مراجعهی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آنرا ذخیره کنید؛ تا کار به روز رسانی و نصب بستهها، به صورت خودکار و همچنین صحیحی انجام شود.
فعال سازی صفحات مخصوص توسعه دهندههای EF Core 1.0
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهندهها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بستهی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحهی نمایش خطاهای بانک اطلاعاتی را میسر میکند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
{ "dependencies": { // same as before "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0" } }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDatabaseErrorPage(); }
تعریف اولین Context برنامه و مشخص سازی رشتهی اتصالی آن
در این تصویر، زیر ساخت نگاشتهای EF Core را مشاهده میکنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرندهی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل اینها را مشاهده میکنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که اینها نیز به فیلدها و ستونهای آن جدول در سمت بانک اطلاعاتی نگاشت میشوند.
بنابراین برای شروع کار، پوشهای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه میکنیم:
namespace Core1RtmEmptyTest.Entities { public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Person> Persons { get; set; } } }
سازندهی این کلاس نیز به نحو خاصی تعریف شدهاست. اگر به سورسهای EF Core مراجعه کنیم، کلاس پایهی DbContext دارای دو سازندهی با و بدون پارامتر است:
protected DbContext() : this((DbContextOptions) new DbContextOptions<DbContext>()) { } public DbContext([NotNull] DbContextOptions options) { // … }
using Microsoft.EntityFrameworkCore; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"... connection string ..."); } } }
اما چون در یک برنامهی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام میشود و در آنجا به سادگی میتوان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشتهی اتصالی را دریافت کرد، مرسوم است از سازندهی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعهی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایلهای config» به فایل appsettings.json مراجعه کرده و تنظیمات رشتهی اتصالی برنامه را به صورت ذیل در آن مشخص میکنیم:
{ "ConnectionStrings": { "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true" } }
در اینجا به وهلهی پیش فرض SQL Server اشاره شدهاست؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده میشود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیدهاست.
پس از تعریف رشتهی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازندهی با پارامتر آن استفاده میکنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویسهای ASP.NET Core معرفی میکنیم:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false) .AddJsonFile($"appsettings.{env}.json", optional: true); Configuration = builder.Build(); } public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; }); services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]); });
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی میکنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.
یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد
با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگیها نیز در آن وجود دارد. یعنی بجای روش فوق، میتوان IConfigurationRoot را به سازندهی کلاس Context برنامه نیز تزریق کرد:
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace Core1RtmEmptyTest.Entities { public class ApplicationDbContext : DbContext { private readonly IConfigurationRoot _configuration; public ApplicationDbContext(IConfigurationRoot configuration) { _configuration = configuration; } //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) //{ //} public DbSet<Person> Persons { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]); } } }
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا میکند و ساده میشود:
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>();
یک نکته: امکان تزریق ApplicationDbContext به تمام کلاسهای برنامه وجود دارد
همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامهی ASP.NET Core نیز وجود دارد:
using System.Linq; using Core1RtmEmptyTest.Entities; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class TestDBController : Controller { private readonly ApplicationDbContext _ctx; public TestDBController(ApplicationDbContext ctx) { _ctx = ctx; } public IActionResult Index() { var name = _ctx.Persons.First().FirstName; return Json(new { firstName = name }); } } }
در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:
علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشدهاست و همچنین ساختار جداول را به آن منتقل نکردهایم که این موارد را در قسمتهای بعدی مرور خواهیم کرد.
This article provides an introduction to a lightweight API for ASP.NET web services provided by DotNetify.SignalR NuGet package that can reduce a lot of boilerplate code when implementing real-time updates, and provide an example of how to create a real-time web component with VueJS without using Node.JS build.
برای تهیه تصاویر سایتهای معرفی شده در قسمت اشتراکهای سایت، پیشتر از کنترل WebBrowser دات نت که در پشت صحنه از امکانات IE کمک میگیرد، استفاده میکردم. بسیار ناپایدار است؛ به روز رسانی مشکلی داشته و وابسته است به سیستم عامل جاری سیستم. برای مثال مرتبا برای تهیه تصاویر بند انگشتی (Thumbnails) سایتهای تهیه شده با بوت استرپ کرش میکرد و این کرش چون از نوع unmaged code است، عملا پروسه IIS وب سایت را از کار میانداخت و در این حالت سایت تا ریاستارت بعدی IIS در دسترس نبود. برای حل این مشکل و بهبود کیفیت تصاویر تهیه شده، از پروژه Awesomium که در حقیقت مرورگر کروم را جهت استفاده در انواع و اقسام زبانهای برنامه نویسی محصور میکند، کمک گرفته شد؛ که شرح آنرا در ادامه ملاحظه خواهید کرد.
دریافت و نصب Awesomium
پروژه Awesomium دارای یک SDK است که از اینجا قابل دریافت میباشد. بعد از نصب آن در مسیر Awesomium SDK\1.7.3.0\wrappers\Awesomium.NET\Assemblies\Packed میتوانید محصور کنندهی دات نتی آنرا مشاهده کنید. منظور از Packed در اینجا، استفاده از DLLهای فشرده شدهی native آن است که در مسیر Awesomium SDK\1.7.3.0\build\bin\packed کپی شدهاند. بنابراین برای توزیع این نوع برنامهها نیاز است اسمبلی دات نتی Awesomium.Core.dll به همراه دو فایل بومی icudt.dll و awesomium.dll ارائه شوند.
تهیه تصاویر سایتها به کمک Awesomium.NET
پس از نصب Awesomium اگر به مسیر Documents\Awesomium SDK Samples\1.7.3.0\Awesomium.NET\Samples\Core\CSharp\BasicSample مراجعه کنید، مثالی را در مورد تهیه تصاویر سایتها به کمک Awesomium.NET، مشاهده خواهید کرد. خلاصهی آن چند سطر ذیل است:
کار با ایجاد یک WebSession شروع میشود. سپس با مقدار دهی CustomCSS، اسکرول بار صفحات را جهت تهیه تصاویری بهتر مخفی میکنیم. سپس یک WebView آغاز شده و منبع آن به Url مدنظر تنظیم میشود. در ادامه باید اندکی صبر کنیم تا بارگذاری سایت خاتمه یافته و نهایتا میتوانیم سطح این View را به صورت یک تصویر ذخیره کنیم.
مشکل! این روش در برنامههای ASP.NET کار نمیکند!
مثال همراه آن یک مثال کنسول ویندوزی است و به خوبی کار میکند؛ اما در برنامههای وب پس از چند روز سعی و خطا مشخص شد که:
الف) WebCore.Shutdown فقط باید در پایان کار یک برنامه فراخوانی شود. یعنی اصلا نیازی نیست تا در برنامههای وب فراخوانی شود.
ب) Awesomium فقط در یک ترد کار میکند. به این معنا که اگر کدهای فوق را در یک صفحهی وب فراخوانی کنید، بار اول کار خواهد کرد. بار دوم برنامه کرش میکند؛ با این پیغام خطا:
چون هر صفحهی وب در یک ترد مجزا اجرا میشود، عملا استفادهی مستقیم از Awesomium در آن ممکن نیست.
خطای فوق هم از آن نوع خطاهایی است که پروسهی IIS را درجا خاموش میکند.
استفاده از Awesomium در یک ترد پس زمینه
راه حلی که نهایتا پاسخ داد و به خوبی و پایدار کار میکند، شامل ایجاد یک ترد مجزای Awesomium در زمان آغاز برنامهی وب و زنده نگه داشتن آن تا زمان پایان کار برنامه است.
در اینجا اگر در برنامههای وب فرم، از طریق HttpContext.Current.Items.Add و یا در برنامههای MVC به کمک this.HttpContext.Items.Add یک آیتم جدید، با کلید Constants.AwesomiumRequest و از نوع کلاس AwesomiumRequest دریافت گردد، مقدار آن به یک ConcurrentQueue اضافه خواهد شد. این صف در یک ترد مجزا مدام در حال بررسی است. اگر مقداری به آن اضافه شدهاست، از صف خارج شده و پردازش خواهد شد.
نمونهی استفاده از آن، در سمت یک برنامهی وب نیز به صورت زیر است. ابتدا ماژول تهیه شده باید در برنامه ثبت شود:
سپس باید تنها مدیریت HttpContext.Current.Items در سمت برنامه صورت گیرد:
در اینجا کاربر درخواست خود را در صف قرار میدهد. پس از مدتی کار آن در WorkerThread ماژول تهیه شده انجام گردیده و تصویر نهایی تهیه میشود.
Url، آدرس وب سایتی است که میخواهید تصویر آن تهیه شود. SavePath مسیر کامل فایل jpg نهایی است که قرار است دریافت و ذخیره گردد. TempDir محل ذخیره سازی فایلهای موقتی Awesomium است. Awesomium یک سری کوکی، تصاویر و فایلهای هر سایت را به این ترتیب کش کرده و در دفعات بعدی سریعتر عمل میکند.
پروژهی کامل آنرا از اینجا میتوانید دریافت کنید:
AwesomiumWebApplication_V1.0.zip
دریافت و نصب Awesomium
پروژه Awesomium دارای یک SDK است که از اینجا قابل دریافت میباشد. بعد از نصب آن در مسیر Awesomium SDK\1.7.3.0\wrappers\Awesomium.NET\Assemblies\Packed میتوانید محصور کنندهی دات نتی آنرا مشاهده کنید. منظور از Packed در اینجا، استفاده از DLLهای فشرده شدهی native آن است که در مسیر Awesomium SDK\1.7.3.0\build\bin\packed کپی شدهاند. بنابراین برای توزیع این نوع برنامهها نیاز است اسمبلی دات نتی Awesomium.Core.dll به همراه دو فایل بومی icudt.dll و awesomium.dll ارائه شوند.
تهیه تصاویر سایتها به کمک Awesomium.NET
پس از نصب Awesomium اگر به مسیر Documents\Awesomium SDK Samples\1.7.3.0\Awesomium.NET\Samples\Core\CSharp\BasicSample مراجعه کنید، مثالی را در مورد تهیه تصاویر سایتها به کمک Awesomium.NET، مشاهده خواهید کرد. خلاصهی آن چند سطر ذیل است:
try { using (WebSession mywebsession = WebCore.CreateWebSession( new WebPreferences() { CustomCSS = "::-webkit-scrollbar { visibility: hidden; }" })) { using (var view = WebCore.CreateWebView(1240, 1000, mywebsession)) { view.Source = new Uri("https://site.com/"); bool finishedLoading = false; view.LoadingFrameComplete += (s, e) => { if (e.IsMainFrame) finishedLoading = true; }; while (!finishedLoading) { Thread.Sleep(100); WebCore.Update(); } using (var surface = (BitmapSurface)view.Surface) { surface.SaveToJPEG("result.jpg"); } } } } finally { WebCore.Shutdown(); }
مشکل! این روش در برنامههای ASP.NET کار نمیکند!
مثال همراه آن یک مثال کنسول ویندوزی است و به خوبی کار میکند؛ اما در برنامههای وب پس از چند روز سعی و خطا مشخص شد که:
الف) WebCore.Shutdown فقط باید در پایان کار یک برنامه فراخوانی شود. یعنی اصلا نیازی نیست تا در برنامههای وب فراخوانی شود.
System.InvalidOperationException: You are attempting to re-initialize the WebCore. The WebCore must only be initialized once per process and must be shut down only when the process exits.
System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt. at Awesomium.Core.NativeMethods.WebCore_CreateWebView_1(HandleRef jarg1, Int32 jarg2, Int32 jarg3, HandleRef jarg4)
خطای فوق هم از آن نوع خطاهایی است که پروسهی IIS را درجا خاموش میکند.
استفاده از Awesomium در یک ترد پس زمینه
راه حلی که نهایتا پاسخ داد و به خوبی و پایدار کار میکند، شامل ایجاد یک ترد مجزای Awesomium در زمان آغاز برنامهی وب و زنده نگه داشتن آن تا زمان پایان کار برنامه است.
using System; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Web; using Awesomium.Core; namespace AwesomiumWebModule { public class AwesomiumModule : IHttpModule { private static readonly Thread WorkerThread = new Thread(awesomiumWorker); private static readonly ConcurrentQueue<AwesomiumRequest> TaskQueue = new ConcurrentQueue<AwesomiumRequest>(); private static bool _isRunning = true; static AwesomiumModule() { WorkerThread.Start(); } private static void awesomiumWorker() { while (_isRunning) { if (TaskQueue.Count != 0) { AwesomiumRequest outRequest; if (TaskQueue.TryDequeue(out outRequest)) { var img = AwesomiumThumbnail.FetchWebPageThumbnail(outRequest); File.WriteAllBytes(outRequest.SavePath, img); Thread.Sleep(500); } } Thread.Sleep(5); } } public void Dispose() { _isRunning = false; WebCore.Shutdown(); } public void Init(HttpApplication context) { context.EndRequest += endRequest; } static void endRequest(object sender, EventArgs e) { var url = HttpContext.Current.Items[Constants.AwesomiumRequest] as AwesomiumRequest; if (url!=null) { TaskQueue.Enqueue(url); } } } }
نمونهی استفاده از آن، در سمت یک برنامهی وب نیز به صورت زیر است. ابتدا ماژول تهیه شده باید در برنامه ثبت شود:
<?xml version="1.0"?> <configuration> <system.web> <compilation debug="true" targetFramework="4.0" /> <httpModules> <add name="AwesomiumWebModule" type="AwesomiumWebModule.AwesomiumModule"/> </httpModules> </system.web> <system.webServer> <validation validateIntegratedModeConfiguration="false"/> <modules> <add name="AwesomiumWebModule" type="AwesomiumWebModule.AwesomiumModule"/> </modules> </system.webServer> </configuration>
protected void btnStart_Click(object sender, EventArgs e) { var host = new Uri(txtUrl.Text).Host; HttpContext.Current.Items.Add(Constants.AwesomiumRequest, new AwesomiumRequest { Url = txtUrl.Text, SavePath = Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data\\Thumbnails\\" + host + ".jpg"), TempDir = Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data\\Temp") }); lblInfo.Text = "Please wait. Your request will be served shortly."; }
Url، آدرس وب سایتی است که میخواهید تصویر آن تهیه شود. SavePath مسیر کامل فایل jpg نهایی است که قرار است دریافت و ذخیره گردد. TempDir محل ذخیره سازی فایلهای موقتی Awesomium است. Awesomium یک سری کوکی، تصاویر و فایلهای هر سایت را به این ترتیب کش کرده و در دفعات بعدی سریعتر عمل میکند.
پروژهی کامل آنرا از اینجا میتوانید دریافت کنید:
AwesomiumWebApplication_V1.0.zip
در این مقاله نمیخواهیم به طور عمیقی وارد جزییاتی مثل توضیح Redis یا کش بشویم؛ فرض شدهاست که کاربر با این مفاهیم آشناست. به طور خلاصه کش کردن یعنی همیشه به دیتابیس یا هارددیسک برای گرفتن اطلاعاتی که میخواهیم و گرفتنش هم کند است، وصل نشویم و بجای آن، اطلاعات را در یک محل موقتی که گرفتنش خیلی سریعتر بوده قرار دهیم و برای استفاده به آنجا برویم و اطلاعات را با سرعت بالا بخوانیم. کش کردن هم دسته بندیهای مختلفی دارد که بر حسب سناریوهای مختلفی که وجود دارد، کاربرد خود را دارند. مثلا سادهترین کش در ASP.NET Core، کش محلی (In-Memory Cache) میباشد که اینترفیس IMemoryCache را اعمال میکند و نیازی به هیچ پکیجی ندارد و به صورت درونی در ASP.NET Core در دسترس است که برای حالت توسعه، یا حالتیکه فقط یک سرور داشته باشیم، مناسب است؛ ولی برای برنامههای چند سروری، نوع دیگری از کش که به اصطلاح به آن Distributed Cache میگویند، بهتر است استفاده شود. چند روش برای پیادهسازی با این ساختار وجود دارد که نکته مشترکشان اعمال اینترفیس واحد IDistributedCache میباشد. در نتیجهی آن، تغییر ساختار کش به روشهای دیگر، که اینترفیس مشابهی را اعمال میکنند، با کمترین زحمت صورت میگیرد. این روشها به طور خیلی خلاصه شامل موارد زیر میباشند:
سپس اینترفیس IResponseCacheService را میسازیم تا از این اینترفیس به جای IDistributedCache استفاده کنیم. البته میتوان از IDistributedCache به طور مستقیم استفاده کرد؛ ولی چون همهی ویژگیهای این اینترفیس را نمیخواهیم و هم اینکه میخواهیم serialize کردن نتایج API را در کلاسی که از این اینترفیس ارثبری میکند (ResponseCacheService) بیاوریم (تا آن را کپسولهسازی (Encapsulation) کرده باشیم تا بعدا بتوانیم مثلا بجای پکیج Newtonsoft.Json، از System.Text.Json برای serialize کردنها استفاده کنیم):
در این کلاس، تزریق وابستگیهای IResponseCacheService و RedisCacheSettings به روش خاصی انجام شده است و نمیتوانستیم از روش Constructor Dependency Injection استفاده کنیم چون در این حالت میبایستی این ورودی در Controller مورد استفاده هم تزریق شود و سپس در اتریبیوت [Cached] بیاید که مجاز به اینکار نیستیم؛ بنابراین از این روش خاص استفاده کردیم. مورد دیگر فرمول ساخت کلید کش میباشد تا بتواند کش بودن یک Endpoint خاص را به طور خودکار تشخیص دهد که این متد در همین کلاس آمده است.
1- Distributed Memory Cache: در واقع Distributed نیست و کش معمولی است؛ فقط برای اعمال اینترفیس IDistributedCache که امکان تغییر آن در ادامهی توسعه نرمافزار میسر باشد، این روش توسط مایکروسافت اضافه شدهاست. نیاز به نصب پکیجی را ندارد و به صورت توکار در ASP.NET Core در دسترس است.
2- Distributed SQL Server Cache: کاربرد چندانی ندارد. با توجه به اینکه هدف اصلی از کش کردن، افزایش سرعت و عدم اتصال به دیتابیس است، استفاده از حافظهی رم، بجای دیتابیس ترجیح داده میشود.
3- Distributed Redis Cache: استفاده از Redis که به طور خلاصه یک دیتابیس Key/Value در حافظه است. سرعت بالایی دارد و محبوبترین روش بین برنامهنویسان است. برای اعمال آن در ASP.NET Core نیاز به نصب پکیج میباشد.
موارد بالا انواع زیرساخت و ساختار (Cache Provider) برای پیادهسازی کش میباشند. روشهای مختلفی برای استفاده از این Cache Providerها وجود دارد. مثلا یک روش، استفاده مستقیم در کدهای درونی متد یا کلاسمان میباشد و یا در روش دیگر میتوانیم به صورت یک Middleware این پروسه را مدیریت کنیم، یا در روش دیگر (که موضوع این مقاله است) از ActionFilterAttribute استفاده میکنیم. یکی از روشهای جالب دیگر کش کردن، اگر از Entity Framework به عنوان ORM استفاده میکنیم، استفاده از سطح دوم کش آن (EF Second Level Cache) میباشد. EF دو سطح کش دارد که سطح اول آن توسط خود Context به صورت درونی استفاده میشود و ما میتوانیم از سطح دوم آن استفاده کنیم. مزیت آن به نسبت روشهای قبلی این است که نتیجهی کوئری ما (که با عبارات لامبدا نوشته میشود) را کش میکند و علاوه بر امکان تنظیم زمان انقضا برای این کش، در صورت تغییر یک entity خاص (انجام عملیات Update/Insert/Delete) خود به خود، کش کوئری مربوط به آن entity پاک میشود تا با مقدار جدید آن جایگزین شود که روشهای دیگر این مزیت را ندارند. در این مقاله قرار نیست در مورد این روش کش صحبت کنیم. استفاده از این روش کش به صورت توکار در EF Core وجود ندارد و برای استفاده از آن در صورتی که از EF Core قبل از ورژن 3 استفاده میکنید میتوانید از پکیج EFSecondLevelCache.Core و در صورت استفاده از EF Core 3 از پکیج EF Core Second Level Cache Interceptor استفاده نمایید که در هر دو حالت میتوان هم از Memory Cache Provider و هم از Redis Cache Provider استفاده نمود.
در این مقاله میخواهیم Responseهای APIهایمان را در یک پروژهی Web API، به سادهترین حالت ممکن کش کنیم. زیرساخت این کش میتواند هر کدام از موارد ذکر شدهی بالا باشد. در این مقاله از Redis برای پیادهسازی آن استفاده میکنیم که با نصب پکیج Microsoft.Extensions.Caching.StackExchangeRedis انجام میگیرد. این بستهی نیوگت که متعلق به مایکروسافت بوده و روش پایهی استفاده از Redis در ASP.NET Core است، اینترفیس IDistributedCache را اعمال میکند:
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
public interface IResponseCacheService { Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive); Task<string> GetCachedResponseAsync(string cacheKey); }
یادآوری: Redis قابلیت ذخیرهی دادههایی از نوع آرایهی بایتها را دارد (و نه هر نوع دلخواهی را). بنابراین اینجا ما بجای ذخیرهی مستقیم نتایج APIهایمان (که ممکن نیست)، میخواهیم ابتدا آنها را با serialize کردن به نوع رشتهای (که فرمت json دارد) تبدیل کنیم و سپس آن را ذخیره نماییم.
حالا کلاس ResponseCacheService که این اینترفیس را اعمال میکند میسازیم:
public class ResponseCacheService : IResponseCacheService, ISingletonDependency { private readonly IDistributedCache _distributedCache; public ResponseCacheService(IDistributedCache distributedCache) { _distributedCache = distributedCache; } public async Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive) { if (response == null) return; var serializedResponse = JsonConvert.SerializeObject(response); await _distributedCache.SetStringAsync(cacheKey, serializedResponse, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = timeToLive }); } public async Task<string> GetCachedResponseAsync(string cacheKey) { var cachedResponse = await _distributedCache.GetStringAsync(cacheKey); return string.IsNullOrWhiteSpace(cachedResponse) ? null : cachedResponse; } }
دقت کنید که اینترفیس IDistributedCache در این کلاس استفاده شده است. اینترفیس ISingletonDependency صرفا یک اینترفیس نشان گذاری برای اعمال خودکار ثبت سرویس به صورت Singleton میباشد (اینترفیس را خودمان ساختهایم و آن را برای رجیستر راحت سرویسهایمان تنظیم کردهایم). اگر نمیخواهید از این روش برای ثبت این سرویس استفاده کنید، میتوانید به صورت عادی این سرویس را رجیستر کنید که در ادامه، در قسمت مربوطه به صورت کامنت شده آمده است.
حالا کدهای لازم برای رجیستر کردن Redis و تنظیمات آن را در برنامه اضافه میکنیم. قدم اول ایجاد یک کلاس POCO به نام RedisCacheSettings است که به فیلدی به همین نام در appsettings.json نگاشت میشود:
public class RedisCacheSettings { public bool Enabled { get; set; } public string ConnectionString { get; set; } public int DefaultSecondsToCache { get; set; } }
این فیلد را در appsettings.json هم اضافه میکنیم تا در استارتاپ برنامه، با مپ شدن به کلاس RedisCacheSettings، قابلیت استفاده شدن در تنظیمات Redis را داشته باشد.
"RedisCacheSettings": { "Enabled": true, "ConnectionString": "192.168.1.107:6379,ssl=False,allowAdmin=True,abortConnect=False,defaultDatabase=0,connectTimeout=500,connectRetry=3", "DefaultSecondsToCache": 600 },
حالا باید سرویس Redis را در متد ConfigureServices، به همراه تنظیمات آن رجیستر کنیم. میتوانیم کدهای مربوطه را مستقیم در متد ConfigureServices بنویسیم و یا به صورت یک متد الحاقی در کلاس جداگانه بنویسیم و از آن در ConfigureServices استفاده کنیم و یا اینکه از روش Installer برای ثبت خودکار سرویس و تنظیماتش استفاده کنیم. اینجا از روش آخر استفاده میکنیم. برای این منظور کلاس CacheInstaller را میسازیم:
public class CacheInstaller : IServiceInstaller { public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly) { var redisCacheService = appSettings.RedisCacheSettings; services.AddSingleton(redisCacheService); if (!appSettings.RedisCacheSettings.Enabled) return; services.AddStackExchangeRedisCache(options => options.Configuration = appSettings.RedisCacheSettings.ConnectionString); // Below code applied with ISingletonDependency Interface // services.AddSingleton<IResponseCacheService, ResponseCacheService>(); } }
خب تا اینجا اینترفیس اختصاصی خودمان را ساختیم و Redis را به همراه تنظیمات آن، رجیستر کردیم. برای اعمال کش، چند روش وجود دارد که همانطور که گفته شد، اینجا از روش ActionFilterAttribute استفاده میکنیم که یکی از راحتترین راههای اعمال کش در APIهای ماست. کلاس CachedAttribute را ایجاد میکنیم:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class CachedAttribute : Attribute, IAsyncActionFilter { private readonly int _secondsToCache; private readonly bool _useDefaultCacheSeconds; public CachedAttribute() { _useDefaultCacheSeconds = true; } public CachedAttribute(int secondsToCache) { _secondsToCache = secondsToCache; _useDefaultCacheSeconds = false; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var cacheSettings = context.HttpContext.RequestServices.GetRequiredService<RedisCacheSettings>(); if (!cacheSettings.Enabled) { await next(); return; } var cacheService = context.HttpContext.RequestServices.GetRequiredService<IResponseCacheService>(); // Check if request has Cache var cacheKey = GenerateCacheKeyFromRequest(context.HttpContext.Request); var cachedResponse = await cacheService.GetCachedResponseAsync(cacheKey); // If Yes => return Value if (!string.IsNullOrWhiteSpace(cachedResponse)) { var contentResult = new ContentResult { Content = cachedResponse, ContentType = "application/json", StatusCode = 200 }; context.Result = contentResult; return; } // If No => Go to method => Cache Value var actionExecutedContext = await next(); if (actionExecutedContext.Result is OkObjectResult okObjectResult) { var secondsToCache = _useDefaultCacheSeconds ? cacheSettings.DefaultSecondsToCache : _secondsToCache; await cacheService.CacheResponseAsync(cacheKey, okObjectResult.Value, TimeSpan.FromSeconds(secondsToCache)); } } private static string GenerateCacheKeyFromRequest(HttpRequest httpRequest) { var keyBuilder = new StringBuilder(); keyBuilder.Append($"{httpRequest.Path}"); foreach (var (key, value) in httpRequest.Query.OrderBy(x => x.Key)) { keyBuilder.Append($"|{key}-{value}"); } return keyBuilder.ToString(); } }
حالا ما میتوانیم با استفاده از attributeی به نام [Cached] که روی APIهای از نوع HttpGet قرار میگیرد آنها را براحتی کش کنیم. کلاس بالا هم طوری طراحی شده (با دو سازنده متفاوت) که در حالت استفاده به صورت [Cached] از مقدار زمان پیشفرضی استفاده میکند که در فایل appsettings.json تنظیم شده است و یا اگر زمان خاصی را مد نظر داشتیم (مثال 1000 ثانیه) میتوانیم آن را به صورت [(Cached(1000] بیاوریم. کلاس زیر نمونهی استفادهی از آن میباشد:
بنابراین وقتی تنظیمات اولیه، برای پیادهسازی این کش انجام شود، اعمال کردن آن به سادگی قرار دادن یک اتریبیوت سادهی [Cached] روی هر apiی است که بخواهیم خروجی آن را کش کنیم. فقط توجه نمایید که این روش فقط برای اکشنهایی که کد 200 را بر میگردانند، یعنی متد Ok را return میکنند (OkObjectResult) کار میکند. بعلاوه اگر از اتریبیوت ApiResultFilter یا مفهوم مشابه آن برای تغییر خروجی API به فرمت خاص استفاده میکنید، باید در آن تغییرات کوچکی را انجام دهید تا با این حالت هماهنگ شود.
[Cached] [HttpGet] public IActionResult Get() { var rng = new Random(); var weatherForecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); return Ok(weatherForecasts); }