namespace TestBackend { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { // ... services.AddHttpContextAccessor(); // + services.AddSession() && app.UseSession() services.AddDbContext<TestContext>((serviceProvider, options) => { options.UseSqlServer(GetConnectionString(serviceProvider)); }); // ... } // ... private string GetConnectionString(IServiceProvider serviceProvider) { var connectionStringTemplate = Configuration.GetConnectionString("ConnectionTemplate"); try { var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>(); // This needs services.AddHttpContextAccessor(); var dbName = httpContextAccessor.HttpContext.Session.GetString("databasename"); // This needs services.AddSession(); && app.UseSession(); return connectionStringTemplate.Replace("{db_Name}", dbName); } catch(Exception ex) { var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(Startup)); logger.LogError("GetConnectionString error", ex, "Failed to get connection string."); } return connectionStringTemplate.Replace("{db_Name}", "---Default-DB-Name-Here---"); } } }
LocalDb دیتابیس توصیه شده برای ویژوال استودیو است و برای انواع پروژهها مانند اپلیکیشنهای وب میتواند استفاده شود. هنگام استفاده از این دیتابیس در IIS Express یا Cassini همه چیز طبق انتظار کار میکند. اما به محض آنکه بخواهید از آن در Full IIS استفاده کنید با خطاهایی مواجه میشوید. مقصود از Full IIS همان نسخه ای است که بهمراه ویندوز عرضه میشود و در قالب یک Windows Service اجرا میگردد.
هنگام استفاده از Full IIS دو خاصیت از LocalDb باعث بروز مشکل میشوند:
- LocalDb نیاز دارد پروفایل کاربر بارگذاری شده باشد
- بصورت پیش فرض، وهله LocalDb متعلق به یک کاربر بوده و خصوصی است
در ادامه این مقاله روی بارگذاری پروفایل کاربر تمرکز میکنیم، و در قسمت بعدی به مالکیت وهله LocalDb میپردازیم.
بارگذاری پروفایل کاربر
بگذارید دوباره به خطای موجود نگاهی بیاندازیم:
System.Data.SqlClient.SqlException: A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 0 - [x89C50120])
این پیغام خطا زیاد مفید نیست، اما LocalDb اطلاعات بیشتری در Event Log ویندوز ذخیره میکند. اگر Windows Logs را باز کنید و به قسمت Application بروید پیغام زیر را مشاهده خواهید کرد.Reported at line: 400
بعلاوه این پیام خطا:
Cannot get a local application data path. Most probably a user profile is not loaded. If LocalDB is executed under IIS, make sure that profile loading is enabled for the current user.
به احتمال زیاد تعداد بیشتری از این دو خطا در تاریخچه وقایع وجود خواهد داشت، چرا که منطق کانکشن ADO.NET چند بار سعی میکند در بازههای مختلف به دیتابیس وصل شود.
پیغام خطای دوم واضح است، نیاز است پروفایل کاربر را بارگذاری کنیم. انجام اینکار زیاد مشکل نیست، هر Application Pool در IIS تنظیماتی برای بارگذاری پروفایل کاربر دارد که از قسمت Advanced Settings قابل دسترسی است. متاسفانه پس از انتشار سرویس پک 1 برای Windows 7 مسائل کمی پیچیدهتر شد. در حال حاظر فعال کردن loadUserProfile برای بارگذاری کامل پروفایل کاربر به تنهایی کافی نیست، و باید setProfileEnvironment را هم فعال کنیم. برای اطلاعات بیشتر در این باره به مستندات KB 2547655 مراجعه کنید. بدین منظور باید فایل applicationHost.config را ویرایش کنید. فایل مذکور در مسیر C:\Windows\System32\inetsrv\config قرار دارد. همانطور که در مستندات KB 2547655 توضیح داده شده، باید پرچم هر دو تنظیمات را برای ASP.NET 4.0 فعال کنیم:
<add name="ASP.NET v4.0" autoStart="true" managedRuntimeVersion="v4.0" managedPipelineMode="Integrated"> <processModel identityType="ApplicationPoolIdentity" loadUserProfile="true" setProfileEnvironment="true" /> </add>
جای هیچ نگرانی نیست، چرا که این پیغام خطا انتظار میرود. همانطور که در ابتدا گفته شد، دو خاصیت LocalDb باعث بروز این خطاها میشوند و ما هنوز به خاصیت دوم نپرداخته ایم. بصورت پیش فرض وهلههای LocalDb خصوصی (private) هستند و در Windows account جاری اجرا میشوند. بنابراین ApplicationPoolIdentity در IIS به وهلههای دیتابیس دسترسی نخواهد داشت. در قسمت دوم این مقاله، راههای مختلفی را برای رفع این مشکل بررسی میکنیم.
- «با HttpHandler بیشتر آشنا شوید»
یکی از بزرگترین تغییرات ASP.NET Core نسبت به نگارشهای قبلی آن، مدیریت HTTP pipeline آن است. به عنوان یک توسعه دهندهی ASP.NET به طور قطع با مفاهیمی مانند HttpHandler و HttpModules آشنایی دارید و ... هر دوی اینها با نگارش جدید ASP.NET حذف و با مفهوم جدیدی به نام Middleware جایگزین شدهاند.
- HttpHandlerها عموما مبتنی بر پسوندهای فایلها عمل میکنند. برای نمونه، رایجترین آنها ASP.NET page handler است که هرگاه درخواستی، ختم شدهی به پسوند aspx، به موتور ASP.NET Web forms وارد شد، به این page handler، جهت پردازش نهایی هدایت میشود. محل تنظیم آنها نیز در فایل web.config است.
- HttpModuleها مبتنی بر رخدادها عمل میکنند. HttpModuleها به عنوان جزئی از request pipeline عمل کرده و دسترسی کاملی دارند به رخدادهای طول عمر درخواست رسیده. آنها را میتوان توسط فایلهای global.asax و یا web.config تنظیم کرد.
- MiddleWareها را که جزئی از طراحی OWIN نیز هستند، میتوان به عنوان کامپوننتهای کوچکی از برنامه که قابلیت یکپارچه شدن با HTTP request pipeline را دارند، درنظر گرفت. عملکرد آنها ترکیبی است از هر دوی HttpHandler و HttpModuleها که در نگارشهای قبلی ASP.NET مورد استفاده بودند. از MiddleWareها میتوان برای پیاده سازی اعمال مختلفی جهت پردازش درخواستهای رسیده مانند اعتبارسنجی، کار با سشنها، ثبت وقایع سیستم، مسیریابی و غیره استفاده کرد. OWIN یا Open Web Interface for .NET به توسعه دهندهها امکان غیروابسته کردن برنامهی ASP.NET خود را از وب سرور مورد استفاده میدهد. به علاوه OWIN امکان پلاگین نویسی اجزای مختلف برنامه را بدون وابسته کردن آنها به یکدیگر فراهم میکند. برای مثال میتوان یک پلاگین logger را تهیه کرد تا اطلاعات مختلفی را از درخواستهای رسیده ثبت کند.
مواردی که با ارائهی ASP.NET Core 1.0 حذف شدهاند
- System.Web: یکی از اهداف اصلی OWIN، مستقل کردن برنامهی وب، از هاست آن است تا بتوان از وب سرورهای بیشتری استفاده کرد. System.Web انحصارا برای IIS طراحی شده بود و در این نگارش دیگر وجود خارجی ندارد.
- HttpModules: با Middlewareها جایگزین شدهاند.
- HttpHandlers: البته HttpHandlers از زمان ارائهی اولین نگارش ASP.NET MVC، در چندین سال قبل منسوخ شدند. زیرا پردازش صفحات وب در ASP.NET MVC برخلاف وب فرمها، از فایلهایی با پسوندهای خاص شروع نمیشوند و نقطهی آغازین آنها، اکشن متدهای کنترلرها است.
- فایل global.asax: با حذف شدن HttpModules، دیگر ضرورتی به وجود این فایل نیست.
شباهتهای بینMiddleware و HttpModuleها
همانند HttpModuleها، Middlewareها نیز به ازای هر درخواست رسیده اجرا میشوند و از هر دو میتوان جهت تولید response ایی خاص استفاده کرد.
تفاوتهای بینMiddleware و HttpModuleها
- عموما HttpModule از طریق web.config و یا فایل global.asax تنظیم میشوند. اما Middlewareها تنها از طریق کد و در فایل Startup.cs برنامههای ASP.NET Core 1.0 قابل معرفی هستند.
- به عنوان یک توسعه دهنده، کنترلی را بر روی ترتیب اجرای انواع و اقسام HttpModuleها نداریم. این مشکل در Middlewareها برطرف شده و اکنون ترتیب اجرای آنها، دقیقا مطابق ترتیب افزوده شدن و تعریف آنها در فایل Startup.cs است.
- ترتیب اجرای HttpModuleها هرچه که باشد، برای حالتهای request و response یکی است. اما ترتیب اجرای Middlewareهای مخصوص response، عکس ترتیب اجرای Middlewareهای مخصوص request هستند (تصویر ذیل).
- HttpModuleها را صرفا جهت اتصال کدهایی به رخدادهای طول عمر برنامه میتوان طراحی کرد؛ اما Middleware مستقل هستند از این رخدادها.
- HttpModuleها به System.Web وابستهاند؛ برخلاف Middlewareها که وابستگی به هاست خود ندارند.
Middlewareهای توکار ASP.NET Core 1.0
ASP.NET Core 1.0 به همراه تعدادی Middleware توکار است؛ مانند:
- Authentication: جهت پشتیبانی از اعتبارسنجی
- CORS: برای فعال سازی Cross-Origin Resource Sharing
- Routing: جهت تعریف و محدود سازی مسیریابیهای برنامه
- Session: برای پشتیبانی و مدیریت سشنهای کاربران
- Diagnostics: پشتیبانی از صفحات خطا و اطلاعات زمان اجرای برنامه
و مواردی دیگر که برای توزیع فایلهای استاتیک و یا مرور محتویات پوشهها و امثال آنها طراحی شدهاند که در طی این مطلب و همچنین مطالب آتی، آنها را بررسی خواهیم کرد.
یک مثال: اگر یک پروژهی خالی ASP.NET Core 1.0 را در ویژوال استودیو ایجاد کنید، این پروژه قادر نیست حتی فایلهای استاتیک مانند تصاویر، فایلهای پیش فرض مانند index.html و یا قابلیت مرور سادهی فایلهای موجود در یک پوشه را ارائه دهد (حتی اگر به آنها نیازی نداشته باشید).
طراحی این نگارش از ASP.NET، مبتنی است بر سبک وزن بودن و ماژولار بودن. هر قابلیتی را که نیاز دارید، باید middleware آنرا نیز خودتان به صورت صریح اضافه کنید و یا در غیر اینصورت فعال نیست. برخلاف نگارشهای قبلی ASP.NET که HTTP Moduleهای از سشن گرفته تا اعتبارسنجی و غیره، همگی به صورت پیش فرض در فایل Web.Config فعال بودند؛ مگر اینکه آنها را به صورت دستی حذف میکردید.
ثبت و فعال سازی اولین Middleware
در ادامهی تکمیل و معرفی برنامهی خالی ASP.NET Core 1.0، قصد داریم یک Middleware توکار را به نام Welcome Page، بجای نمایش سطر Hello world پیش فرض این برنامه، ثبت و فعال سازی کنیم. این Middleware ویژه، در اسمبلی به نام Microsoft.AspNetCore.Diagnostics قرار دارد. اگر فایل project.json پیش فرض این پروژه را باز کنید، این اسمبلی به صورت پیش فرض در آن ثبت و معرفی شدهاست:
{ "dependencies": { "Microsoft.NETCore.App": { "version": "1.0.0", "type": "platform" }, "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", "Microsoft.Extensions.Logging.Console": "1.0.0" },
پس از اطمینان حاصل کردن از نصب بستهی نیوگت Microsoft.AspNetCore.Diagnostics، اکنون جهت معرفی Middleware توکار Welcome Page ، فایل Startup.cs را گشوده و کدهای آنرا به نحو ذیل تغییر دهید:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; namespace Core1RtmEmptyTest { public class Startup { public void ConfigureServices(IServiceCollection services) { } public void Configure(IApplicationBuilder app) { app.UseWelcomePage(); app.Run(async (context) => { await context.Response.WriteAsync("Hello DNT!"); }); } } }
به علاوه middleware دومی را نیز با متد الحاقی Run مشاهده میکنید. به این نوع middlewareهای خاص، اصطلاحا terminal middleware میگویند. از این جهت که درخواستی را دریافت و یک response را تولید میکنند و کار همینجا خاتمه مییابد و زنجیرهی پردازشی middlewareها ادامه نخواهد یافت. در اینجا پارامتر context آن از نوع HttpContext است و باید دقت داشت، زمانیکه کار نوشتن در Response، در اینجا انجام شد، اگر پس از متد Run یک Middleware دیگر را ثبت کنید، هیچگاه اجرا نخواهد شد.
در این حالت اگر برنامه را اجرا کنید، خروجی ذیل را مشاهده خواهید کرد:
welcome page نیز یک terminal middleware است. به همین جهت middleware بعدی ثبت شدهی در اینجا یا همان متد Run، دیگر اجرا نخواهد شد.
در قسمتهای بعدی، تعداد بیشتری از Middlewareهای توکار ASP.NET Core 1.0 را بررسی خواهیم کرد.
نام این جدول را با درنظر گرفتن شرایط موجود میتوان Resources گذاشت.
ستون Name برای ذخیره نام منبع درنظر گرفته شده است. این نام برابر نام منابع درخواستی در سیستم مدیریت منابع ASP.NET است که درواقع برابر همان نام فایل منبع اما بدون پسوند resx. است.
ستون Key برای نگهداری کلید ورودی منبع استفاده میشود که دقیقا برابر همان مقداری است که درون فایلهای resx. ذخیره میشود.
ستون Culture برای ذخیره کالچر ورودی منبع به کار میرود. این مقدار میتواند برای کالچر پیشفرض برنامه برابر رشته خالی باشد.
ستون Value نیز برای نگهداری مقدار ورودی منبع استفاده میشود.
برای ستون Id میتوان از GUID نیز استفاده کرد. در اینجا برای راحتی کار از نوع داده bigint و خاصیت Identity برای تولید خودکار آن در Sql Server استفاده شده است.
نکته: برای امنیت بیشتر میتوان یک Unique Constraint بر روی سه فیلد Name و Key و Culture اعمال کرد.
برای نمونه به تصویر زیر که ذخیره تعدای ورودی منبع را درون جدول Resources نمایش میدهد دقت کنید:
اصلاح کلاس DbResourceProviderFactory
برای ذخیره منابع محلی، جهت اطمینان از یکسان بودن نام منبع، متد مربوطه در کلاس DbResourceProviderFactory باید بهصورت زیر تغییر کند:
public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { if (!string.IsNullOrEmpty(virtualPath)) { virtualPath = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); // removes everything from start to the first '/' } return new LocalDbResourceProvider(virtualPath); }
ارتباط با دیتابیس
خوشبختانه برای تبادل اطلاعات با جدول بالا امروزه راههای زیادی وجود دارد. برای پیادهسازی آن مثلا میتوان از یک اینترفیس استفاده کرد. سپس با استفاده از سازوکارهای موجود مثلا بهکارگیری IoC، نمونه مناسبی از پیادهسازی اینترفیس مذبور را در اختیار برنامه قرار داد.
اما برای جلوگیری از پیچیدگی بیش از حد و دور شدن از مبحث اصلی، برای پیادهسازی فعلی از EF Code First به صورت مستقیم در پروژه استفاده شده است که سری آموزشی کاملی از آن در همین سایت وجود دارد.
پس از پیادهسازی کلاسهای مرتبط برای استفاده از EF Code First، از کلاس ResourceData که در بخش اول نیز نشان داده شده بود، برای کپسوله کردن ارتباط با دادهها استفاده میشود که نمونهای ابتدایی از آن در زیر آورده شده است:
using System.Collections.Generic; using System.Linq; using DbResourceProvider.Models; namespace DbResourceProvider.Data { public class ResourceData { private readonly string _resourceName; public ResourceData(string resourceName) { _resourceName = resourceName; } public Resource GetResource(string resourceKey, string culture) { using (var data = new TestContext()) { return data.Resources.SingleOrDefault(r => r.Name == _resourceName && r.Key == resourceKey && r.Culture == culture); } } public List<Resource> GetResources(string culture) { using (var data = new TestContext()) { return data.Resources.Where(r => r.Name == _resourceName && r.Culture == culture).ToList(); } } } }
using System.Collections.Generic; using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceManager { private readonly string _resourceName; private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture; public DbResourceManager(string resourceName) { _resourceName = resourceName; _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>(); } public object GetObject(string resourceKey, CultureInfo culture) { return GetCachedObject(resourceKey, culture.Name); } private object GetCachedObject(string resourceKey, string cultureName) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResource = _resourceCacheByCulture[cultureName]; lock (this) { if (!cachedResource.ContainsKey(resourceKey)) { var data = new ResourceData(_resourceName); var dbResource = data.GetResource(resourceKey, cultureName); if (dbResource == null) return null; var cachedResources = _resourceCacheByCulture[cultureName]; cachedResources.Add(dbResource.Key, dbResource.Value); } } return cachedResource[resourceKey]; } } }
private object GetCachedObject(string resourceKey, string cultureName) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) { _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResources = _resourceCacheByCulture[cultureName]; var data = new ResourceData(_resourceName); var dbResources = data.GetResources(cultureName); foreach (var dbResource in dbResources) { cachedResources.Add(dbResource.Key, dbResource.Value); } } } var cachedResource = _resourceCacheByCulture[cultureName]; return !cachedResource.ContainsKey(resourceKey) ? null : cachedResource[resourceKey]; }
using System.Collections; using System.Collections.Generic; using System.Globalization; namespace DbResourceProvider { public class CultureFallbackProvider : IEnumerable<CultureInfo> { private readonly CultureInfo _startingCulture; private readonly CultureInfo _neutralCulture; private readonly bool _tryParentCulture; public CultureFallbackProvider(CultureInfo startingCulture = null, CultureInfo neutralCulture = null, bool tryParentCulture = true) { _startingCulture = startingCulture ?? CultureInfo.CurrentUICulture; _neutralCulture = neutralCulture; _tryParentCulture = tryParentCulture; } #region Implementation of IEnumerable<CultureInfo> public IEnumerator<CultureInfo> GetEnumerator() { var reachedNeutralCulture = false; var currentCulture = _startingCulture; do { if (_neutralCulture != null && currentCulture.Name == _neutralCulture.Name) { yield return CultureInfo.InvariantCulture; reachedNeutralCulture = true; break; } yield return currentCulture; currentCulture = currentCulture.Parent; } while (_tryParentCulture && !HasInvariantCultureName(currentCulture)); if (!_tryParentCulture || HasInvariantCultureName(_startingCulture) || reachedNeutralCulture) yield break; yield return CultureInfo.InvariantCulture; } #endregion #region Implementation of IEnumerable IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion private bool HasInvariantCultureName(CultureInfo culture) { return culture.Name == CultureInfo.InvariantCulture.Name; } } }
public object GetObject(string resourceKey, CultureInfo culture) { foreach (var currentCulture in new CultureFallbackProvider(culture)) { var value = GetCachedObject(resourceKey, currentCulture.Name); if (value != null) return value; } throw new KeyNotFoundException("The specified 'resourceKey' not found."); }
ابتدا تنظیمات موردنیاز فایل کانفیگ را که در قسمت قبل نشان داده شد، در برنامه خود اعمال کنید.
دادههای نمونه نشان داده شده در ابتدای این مطلب را درنظر بگیرید. حال اگر در یک برنامه وب اپلیکیشن، صفحه Default.aspx در ریشه سایت حاوی دو کنترل زیر باشد:
<asp:Label ID="Label1" runat="server" meta:resourcekey="Label1" /> <asp:Label ID="Label2" runat="server" meta:resourcekey="Label2" />
سپس تغییر زیر را در فایل web.config اعمال کنید تا کالچر UI سایت به fa تغییر یابد (به بخش "uiCulture="fa دقت کنید):
<globalization uiCulture="fa" resourceProviderFactoryType = "DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
میبینید که با توجه به عدم وجود مقداری برای Label2.Text برای کالچر fa، عملیات fallback اتفاق افتاده است.
کار تولید یک پرووایدر منابع سفارشی دیتابیسی به اتمام رسید. تا اینجا اصول کلی تولید یک پرووایدر سفارشی شرح داده شد. بدین ترتیب میتوان برای هر حالت خاص دیگری نیز پرووایدرهای سفارشی مخصوص ساخت تا مدیریت منابع به آسانی تحت کنترل برنامه نویس قرار گیرد.
اما نکتهای را که باید به آن توجه کنید این است که در پیادهسازیهای نشان داده شده با توجه به نحوه کششدن مقادیر ورودیها، اگر این مقادیر در دیتابیس تغییر کنند، تا زمانیکه سایت ریست نشود این تغییرات در برنامه اعمال نخواهد شد. زیرا همانطور که اشاره شد، مدیریت نمونههای تولیدشده از پرووایدرهای منابع برای هر منبع درخواستی درنهایت برعهده ASP.NET است. بنابراین باید مکانیزمی پیاده شود تا کلاس DbResourceManager از بهروزرسانی ورودیهای کششده اطلاع یابد تا آنها را ریفرش کند.
در ادامه درباره روشهای مختلف نحوه پیادهسازی قابلیت بهروزرسانی ورودیهای منابع در زمان اجرا با استفاده از پرووایدرهای منابع سفارشی بحث خواهد شد. همچنین راهحلهای مختلف استفاده از این پرووایدرهای سفارشی در جاهای مختلف پروژههای MVC شرح داده میشود.
البته مباحث پیشرفتهتری چون تزریق وابستگی برای پیادهسازی لایه ارتباط با دیتابیس در بیرون و یا تولید یک Factory برای تزریق کامل پرووایدر منابع از بیرون نیز جای بحث و بررسی دارد.
منابع
http://msdn.microsoft.com/en-us/library/aa905797.aspx
http://msdn.microsoft.com/en-us/library/system.web.compilation.resourceproviderfactory.aspx
http://www.dotnetframework.org/default.aspx/.../ResourceFallbackManager@cs
http://www.codeproject.com/Articles/14190/ASP-NET-2-0-Custom-SQL-Server-ResourceProvider
الف) نیاز است ارائه تصاویر تحت کنترل برنامه باشند.
using System.IO; using System.Net.Mime; using System.Web.Mvc; namespace MvcWatermark.Controllers { public class HomeController : Controller { const int ADay = 86400; public ActionResult Index() { return View(); } [OutputCache(VaryByParam = "fileName", Duration = ADay)] public ActionResult Image(string fileName) { fileName = Path.GetFileName(fileName); // تمیز سازی امنیتی است var rootPath = Server.MapPath("~/App_Data/Images"); var path = Path.Combine(rootPath, fileName); if (!System.IO.File.Exists(path)) { var notFoundImage = "notFound.png"; path = Path.Combine(rootPath, notFoundImage); return File(path, MediaTypeNames.Image.Gif, notFoundImage); } return File(path, MediaTypeNames.Image.Gif, fileName); } } }
نحوه استفاده از این اکشن متد نیز به نحو زیر است:
<img src="@Url.Action(actionName: "Image", controllerName: "Home", routeValues: new { fileName = "EF_Stra_08.gif" })" />
ب) آیا فراخوان تصویر ما را مستقیما در سایت خودش قرار داده است؟
private bool isEmbeddedIntoAnotherDomain { get { return this.HttpContext.Request.UrlReferrer != null && !this.HttpContext.Request.Url.Host.Equals(this.HttpContext.Request.UrlReferrer.Host, StringComparison.InvariantCultureIgnoreCase); } }
ج) افزودن خودکار Watermark در صورت کپی شدن در سایتی دیگر
private byte[] addWaterMark(string filePath, string text) { var image = new WebImage(filePath); image.AddTextWatermark(text); return image.GetBytes(); }
اما ... پس از امتحان تصاویر مختلف ممکن است گاها با خطای زیر مواجه شویم:
A Graphics object cannot be created from an image that has an indexed pixel format.
PixelFormatUndefined PixelFormatDontCare PixelFormat1bppIndexed PixelFormat4bppIndexed PixelFormat8bppIndexed PixelFormat16bppGrayScale PixelFormat16bppARGB1555
private byte[] addWaterMark(string filePath, string text) { using (var img = System.Drawing.Image.FromFile(filePath)) { using (var memStream = new MemoryStream()) { using (var bitmap = new Bitmap(img))//avoid gdi+ errors { bitmap.Save(memStream, ImageFormat.Png); var webImage = new WebImage(memStream); webImage.AddTextWatermark(text, verticalAlign: "Top", horizontalAlign: "Left", fontColor: "Brown"); return webImage.GetBytes(); } } } }
در ادامه، قسمت آخر کار، اعمال این مراحل به اکشن متد Image است:
if (isEmbeddedIntoAnotherDomain) { var text = Url.Action(actionName: "Index", controllerName: "Home", routeValues: null, protocol: "http"); var content = addWaterMark(path, text); return File(content, MediaTypeNames.Image.Gif, fileName); } return File(path, MediaTypeNames.Image.Gif, fileName);
کدهای نهایی این کنترلر را از اینجا میتوانید دریافت کنید:
HomeController.cs
به همراه نمونه تصویری که استثنای یاد شده را تولید میکند؛ جهت آزمایش بیشتر:
EFStra08.gif
به طور مثال در کلاس بالا یک کارمند میتواند فروشنده یا مهندس باشد. پیاده سازی بالا این مورد را با استفاده از دو فیلد نشان دادهاست که در صورت true بودن، مقدار هریک از آنها، نوع کلاس متناظر با آن خواهد بود.
مثلا اگر IsSalesman مقدار true داشته باشد، شیء ما کارمندی با نقش Salesman است و در صورتی که IsEngineer مقدار true داشته باشد ، شیء کارمند، نقش مهندس دارد. بماند که حالتهای دیگری نیز برای مقادیر این فیلدها وجود دارند!
تا اینجا مشکل خاصی وجود ندارد؛ بجز کمی ناخوانا شدن کد و کثیف کاری. اما مشکل اساسی زمانی پیش میآید که کلاس Employee نیاز به پیاده سازی رفتارهایی مختص به هر یک از این انواع را پیدا کند. به طور مثال شرایط کاری و امکانات مورد نیاز یک مهندس، با فروشنده متفاوت است و مثلا هنگام ثبت حکم یک فرد، نیاز به بررسی شرایط متفاوتی نسبت به نوع یک کارمند، وجود داشته باشد.
اگر با همین فرمان کدنویسی را ادامه دهیم احتمالا با کلاسی روبرو خواهیم شد که پر است از گذارههای if else ، switch و یا مواردی از این دست؛ که ابدا شرایط دلپذیری برای دوستانی که قصد نگهداری از کد ما را دارند، نیست!
زمانیکه با چنین موردی مواجه میشوید. ابتدا به ارتباط معنایی نوعهای به کار رفتهی در کلاس توجه کنید. در صورتیکه میتوان این انواع را به صورت polymorphic طراحی مجدد کرد، حتما این کار را انجام دهید. البته به ندرت مشاهده کردهام چنین چیزی امکان نداشته باشد. در صورتیکه ارتباط معنایی خاصی وجود نداشته باشد، میتوانید با استفاده از دیگر بازسازیهای کد، کلاسها را جدا کرده و دو کلاس مجزا را ایجاد نمایید. یا با استفاده از دیگر بازسازیهای کد که در آینده خواهم گفت، به طریق دیگری کد را تغییر دهید که خدا را هم خوش بیاید. به طور مثال طراحی زیر میتواند نتیجه بازسازی کلاس بالا با روش ذکر شده باشد.
مراحل انجام این بازسازی کد
- اگر type کد درونی کلاس از طریق سازنده به کلاس ارسال شده است، این سازنده را با یک متد سازنده (Factory method) جایگزین کنید.
- به ازای هر مقدار از type کدهای درونی کلاس، یک زیر کلاس جدید بسازید.
- تمامی استفادهها از type کدهای درونی کلاس را بازسازی کرده و به کلاسهای مربوط به خود منتقل کنید (احتمالا تمامی پیاده سازیهایی که if else یا switch ای بر روی مقدار type کدها دارند).
- از کلاس پایه، type کد را حذف کنید.
- در صورت وجود تمامی استفادهها از سازنده، کلاس اولیه را به استفاده از متد سازنده تغییر دهید.
- کد را کامپایل و تست نمایید.
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت دوازدهم- یکپارچه سازی با اکانت گوگل
ثبت یک برنامهی جدید در گوگل
اگر بخواهیم از گوگل به عنوان یک IDP ثالث در IdentityServer استفاده کنیم، نیاز است در ابتدا برنامهی IDP خود را به آن معرفی و در آنجا ثبت کنیم. برای این منظور مراحل زیر را طی خواهیم کرد:
1- مراجعه به developer console گوگل و ایجاد یک پروژهی جدید
https://console.developers.google.com
در صفحهی باز شده، بر روی دکمهی select project در صفحه و یا لینک select a project در نوار ابزار آن کلیک کنید. در اینجا دکمهی new project و یا create را مشاهده خواهید کرد. هر دوی این مفاهیم به صفحهی زیر ختم میشوند:
در اینجا نامی دلخواه را وارد کرده و بر روی دکمهی create کلیک کنید.
2- فعالسازی API بر روی این پروژهی جدید
در ادامه بر روی لینک Enable APIs And Services کلیک کنید و سپس google+ api را جستجو نمائید.
پس از ظاهر شدن آن، این گزینه را انتخاب و در صفحهی بعدی، آنرا با کلیک بر روی دکمهی enable، فعال کنید.
3- ایجاد credentials
در اینجا بر روی دکمهی create credentials کلیک کرده و در صفحهی بعدی، این سه گزینه را با مقادیر مشخص شده، تکمیل کنید:
• Which API are you using? – Google+ API • Where will you be calling the API from? – Web server (e.g. node.js, Tomcat) • What data will you be accessing? – User data
• نام: همان مقدار پیشفرض آن
• Authorized JavaScript origins: آنرا خالی بگذارید.
• Authorized redirect URIs: این مورد همان callback address مربوط به IDP ما است که در اینجا آنرا با آدرس زیر مقدار دهی خواهیم کرد.
https://localhost:6001/signin-google
سپس در ذیل این صفحه بر روی دکمهی «Create OAuth 2.0 Client ID» کلیک کنید تا به صفحهی «Set up the OAuth 2.0 consent screen» بعدی هدایت شوید. در اینجا دو گزینهی آنرا به صورت زیر تکمیل کنید:
- Email address: همان آدرس ایمیل واقعی شما است.
- Product name shown to users: یک نام دلخواه است. نام برنامهی خود را برای نمونه ImageGallery وارد کنید.
برای ادامه بر روی دکمهی Continue کلیک نمائید.
4- دریافت credentials
در پایان این گردش کاری، به صفحهی نهایی «Download credentials» میرسیم. در اینجا بر روی دکمهی download کلیک کنید تا ClientId و ClientSecret خود را توسط فایلی به نام client_id.json دریافت نمائید.
سپس بر روی دکمهی Done در ذیل صفحه کلیک کنید تا این پروسه خاتمه یابد.
تنظیم برنامهی IDP برای استفادهی از محتویات فایل client_id.json
پس از پایان عملیات ایجاد یک برنامهی جدید در گوگل و فعالسازی Google+ API در آن، یک فایل client_id.json را دریافت میکنیم که اطلاعات آن باید به صورت زیر به فایل آغازین برنامهی IDP اضافه شود:
الف) تکمیل فایل src\IDP\DNT.IDP\appsettings.json
{ "Authentication": { "Google": { "ClientId": "xxxx", "ClientSecret": "xxxx" } } }
ب) تکمیل اطلاعات گوگل در کلاس آغازین برنامه
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthentication() .AddGoogle(authenticationScheme: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Authentication:Google:ClientId"]; options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; }); }
- authenticationScheme تنظیم شده باید یک عبارت منحصربفرد باشد.
- همچنین SignInScheme یک چنین مقداری را در اصل دارد:
public const string ExternalCookieAuthenticationScheme = "idsrv.external";
آزمایش اعتبارسنجی کاربران توسط اکانت گوگل آنها
اکنون که تنظیمات اکانت گوگل به پایان رسید و همچنین به برنامه نیز معرفی شد، برنامهها را اجرا کنید. مشاهده خواهید کرد که امکان لاگین توسط اکانت گوگل نیز به صورت خودکار به صفحهی لاگین IDP ما اضافه شدهاست:
در اینجا با کلیک بر روی دکمهی گوگل، به صفحهی لاگین آن که به همراه نام برنامهی ما است و انتخاب اکانتی از آن هدایت میشویم:
پس از آن، از طرف گوگل به صورت خودکار به IDP (همان آدرسی که در فیلد Authorized redirect URIs وارد کردیم)، هدایت شده و callback رخداده، ما را به سمت صفحهی ثبت اطلاعات کاربر جدید هدایت میکند. این تنظیمات را در قسمت قبل ایجاد کردیم:
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class ExternalController : Controller { public async Task<IActionResult> Callback() { var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user = AutoProvisionUser(provider, providerUserId, claims); var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl }); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" , new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId }); return Redirect(continueWithUrl); }
در اینجا نحوهی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحهی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده میکنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد و به برنامه با این هویت جدید وارد میشود.
اتصال کاربر وارد شدهی از طریق یک IDP خارجی به اکانتی که هم اکنون در سطح IDP ما موجود است
تا اینجا اگر کاربری از طریق یک IDP خارجی به برنامه وارد شود، او را به صفحهی ثبت نام کاربر هدایت کرده و پس از دریافت اطلاعات او، اکانت خارجی او را به اکانتی جدید که در IDP خود ایجاد میکنیم، متصل خواهیم کرد. به همین جهت بار دومی که این کاربر به همین ترتیب وارد سایت میشود، دیگر صفحهی ثبت نام و تکمیل اطلاعات را مشاهده نمیکند. اما ممکن است کاربری که برای اولین بار از طریق یک IDP خارجی به سایت ما وارد شدهاست، هم اکنون دارای یک اکانت دیگری در سطح IDP ما باشد؛ در اینجا فقط اتصالی بین این دو صورت نگرفتهاست. بنابراین در این حالت بجای ایجاد یک اکانت جدید، بهتر است از همین اکانت موجود استفاده کرد و صرفا اتصال UserLogins او را تکمیل نمود.
به همین جهت ابتدا نیاز است لیست Claims بازگشتی از گوگل را بررسی کنیم:
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); foreach (var claim in claims) { _logger.LogInformation($"External provider[{provider}] info-> claim:{claim.Type}, value:{claim.Value}"); }
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name, value:Vahid N. External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname, value:Vahid External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname, value:N. External provider[Google] info-> claim:urn:google:profile, value:https://plus.google.com/105013528531611201860 External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, value:my.name@gmail.com
[HttpGet] public async Task<IActionResult> Callback() { // ... var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user wasn't found by provider, but maybe one exists with the same email address? if (provider == "Google") { // email claim from Google var email = claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"); if (email != null) { var userByEmail = await _usersService.GetUserByEmailAsync(email.Value); if (userByEmail != null) { // add Google as a provider for this user await _usersService.AddUserLoginAsync(userByEmail.SubjectId, provider, providerUserId); // redirect to ExternalLoginCallback var continueWithUrlAfterAddingUserLogin = Url.Action("Callback", new {returnUrl = returnUrl}); return Redirect(continueWithUrlAfterAddingUserLogin); } } } var returnUrlAfterRegistration = Url.Action("Callback", new {returnUrl = returnUrl}); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration", new {returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId}); return Redirect(continueWithUrl); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
زمانی که برای احراز هویت از ASP.NET Identity استفاده میکنید، ممکن است برای شما هم پیش آمده باشد که اگر در یکی از سایتهای خود لاگین کرده باشید و سایتی دیگر را که از ASP.NET Identity استفاده میکند، باز کنید بدون اینکه لازم باشد دوباره لاگین کنید، با همان نام کاربری سایت اول در سایت دوم هم وارد شده اید (بر روی یک دومین یا هاست شدهی در یک سیستم). برای نمونه میتوانید دو برنامه asp.net mvc ایجاد کنید، سپس یکی را اجرا کنید و پس از ثبت نام در آن و لاگین کردن، برنامه دوم را اجرا کنید، احتمالا مشاهده میکنید که به صورت خودکار وارد شدهاید.
دلیل و راه حل آن ساده است، AspNet.ApplicationCookie در هر دوسایت هم نام است، آن را تغییر دهید:
public partial class Startup { // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864 public void ConfigureAuth(IAppBuilder app) { // Enable the application to use a cookie to store information for the signed in user app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), CookieName = "MyCookieName", }); } }
عموما در ORMها دو سطح کش میتواند وجود داشته باشد:
الف) سطح اول کش
که نمونه بارز آن در EF Code first استفاده از متد context.Entity.Find است. در بار اول فراخوانی این متد، مراجعهای به بانک اطلاعاتی صورت گرفته تا بر اساس primary key ذکر شده در آرگومان آن، رکورد متناظری بازگشت داده شود. در بار دوم فراخوانی متد Find، دیگر مراجعهای به بانک اطلاعاتی صورت نخواهد گرفت و اطلاعات از سطح اول کش (یا همان Context جاری) خوانده میشود.
بنابراین سطح اول کش در طول عمر یک تراکنش معنا پیدا میکند و به صورت خودکار توسط EF مدیریت میشود.
ب) سطح دوم کش
سطح دوم کش در ORMها طول عمر بیشتری داشته و سراسری است. هدف از آن کش کردن اطلاعات عمومی و پر مصرفی است که در دید تمام کاربران قرار دارد و همچنین تمام کاربران میتوانند به آن دسترسی داشته باشند. بنابراین محدود به یک Context نیست.
عموما پیاده سازی سطح دوم کش خارج از ORM مورد استفاده قرار میگیرد و توسط اشخاص و شرکتهای ثالث تهیه میشود.
در حال حاضر پیاده سازی توکاری از سطح دوم کش در EF Code first وجود ندارد و قصد داریم در مطلب جاری به یک پیاده سازی نسبتا خوب از آن برسیم.
تلاشهای صورت گرفته
تا کنون دو پیاده سازی نسبتا خوب از سطح دوم کش در EF صورت گرفته:
Entity Framework Code First Caching
Caching the results of LINQ queries
مورد اول برای ایده گرفتن خوب است. بحث اصلی پیاده سازی سطح دوم کش، یافتن کلیدی است که معادل کوئری LINQ در حال فراخوانی است. سطح دوم کش را به صورت یک Dictionary تصور کنید. هر آیتم آن تشکیل شده است از یک کلید و یک مقدار. از کلید برای یافتن مقدار متناظر استفاده میشود.
اکنون مشکل چیست؟ در یک برنامه ممکن است صدها کوئری لینک وجود داشته باشد. چطور باید به ازای هر کوئری LINQ یک کلید منحصربفرد تولید کرد؟
در مطلب «Entity Framework Code First Caching» از متد ToString استفاده شده است. اگر این متد، بر روی یک عبارت LINQ در EF Code first فراخوانی شود، معادل SQL آن نمایش داده میشود. بنابراین یک قدم به تولید کلید منحصربفرد متناظر با یک کوئری نزدیک شدهایم. اما ... مشکل اینجا است که متد ToString پارامترها را لحاظ نمیکند. بنابراین این روش اصلا قابل استفاده نیست. چون کاربر به ازای تمام پارامترهای ارسالی، همواره یک نتیجه را دریافت خواهد کرد.
در مقاله «Caching the results of LINQ queries» این مشکل برطرف شده است. با parse کامل expression tree یک عبارت LINQ کلید منحصربفرد معادل آن یافت میشود. سپس بر این اساس میتوان نتیجه کوئری را به نحو صحیحی کش کرد. در این روش پارامترها هم لحاظ میشوند و مشکل مقاله قبلی را ندارد.
اما این مقاله دوم یک مشکل مهم را به همراه دارد: روشی را برای حذف آیتمها از کش ارائه نمیدهد. فرض کنید مقالات سایت را در سطح دوم کش قرار دادهاید. اکنون یک مقاله جدید در سایت ثبت شده است. اصطلاحا برای invalidating کش در این روش، راهکاری پیشنهاد نشده است.
پیاده سازی بهتری از سطح دوم کش در EF Code fist
میتوان از همان روش یافتن کلید منحصربفرد معادل با یک کوئری LINQ، که در مقاله دوم فوق، یاد شد، کار را شروع کرد و سپس آنرا به مرحلهای رساند که مباحث حذف کش نیز به صورت خودکار مدیریت شود. پیاده سازی آن را برای برنامههای وب در ذیل ملاحظه میکنید:
using System; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Data.Objects; using System.Diagnostics; using System.Linq; using System.Web; using System.Web.Caching; namespace EfSecondLevelCaching.Core { public static class EfHttpRuntimeCacheProvider { #region Methods (6) // Public Methods (2) public static IList<TEntity> ToCacheableList<TEntity>( this IQueryable<TEntity> query, int durationMinutes = 15, CacheItemPriority priority = CacheItemPriority.Normal) { return query.Cacheable(x => x.ToList(), durationMinutes, priority); } /// <summary> /// Returns the result of the query; if possible from the cache, otherwise /// the query is materialized and the result cached before being returned. /// The cache entry has a one minute sliding expiration with normal priority. /// </summary> public static TResult Cacheable<TEntity, TResult>( this IQueryable<TEntity> query, Func<IQueryable<TEntity>, TResult> materializer, int durationMinutes = 15, CacheItemPriority priority = CacheItemPriority.Normal) { // Gets a cache key for a query. var queryCacheKey = query.GetCacheKey(); // The name of the cache key used to clear the cache. All cached items depend on this key. var rootCacheKey = typeof(TEntity).FullName; // Try to get the query result from the cache. printAllCachedKeys(); var result = HttpRuntime.Cache.Get(queryCacheKey); if (result != null) { debugWriteLine("Fetching object '{0}__{1}' from the cache.", rootCacheKey, queryCacheKey); return (TResult)result; } // Materialize the query. result = materializer(query); // Adding new data. debugWriteLine("Adding new data: queryKey={0}, dependencyKey={1}", queryCacheKey, rootCacheKey); storeRootCacheKey(rootCacheKey); HttpRuntime.Cache.Insert( key: queryCacheKey, value: result, dependencies: new CacheDependency(null, new[] { rootCacheKey }), absoluteExpiration: DateTime.Now.AddMinutes(durationMinutes), slidingExpiration: Cache.NoSlidingExpiration, priority: priority, onRemoveCallback: null); return (TResult)result; } /// <summary> /// Call this method in `public override int SaveChanges()` of your DbContext class /// to Invalidate Second Level Cache automatically. /// </summary> public static void InvalidateSecondLevelCache(this DbContext ctx) { var changedEntityNames = ctx.ChangeTracker .Entries() .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified || x.State == EntityState.Deleted) .Select(x => ObjectContext.GetObjectType(x.Entity.GetType()).FullName) .Distinct() .ToList(); if (!changedEntityNames.Any()) return; printAllCachedKeys(); foreach (var item in changedEntityNames) { item.removeEntityCache(); } printAllCachedKeys(); } // Private Methods (4) private static void debugWriteLine(string format, params object[] args) { if (!Debugger.IsAttached) return; Debug.WriteLine(format, args); } private static void printAllCachedKeys() { if (!Debugger.IsAttached) return; debugWriteLine("Available cached keys list:"); int count = 0; var enumerator = HttpRuntime.Cache.GetEnumerator(); while (enumerator.MoveNext()) { if (enumerator.Key.ToString().StartsWith("__")) continue; // such as __System.Web.WebPages.Deployment debugWriteLine("queryKey: {0}", enumerator.Key.ToString()); count++; } debugWriteLine("count: {0}", count); } private static void removeEntityCache(this string rootCacheKey) { if (string.IsNullOrWhiteSpace(rootCacheKey)) return; debugWriteLine("Removing items with dependencyKey={0}", rootCacheKey); // Removes all cached items depend on this key. HttpRuntime.Cache.Remove(rootCacheKey); } private static void storeRootCacheKey(string rootCacheKey) { // The cacheKeys of a cacheDependency that are not already in cache ARE NOT inserted into the cache // on the Insert of the item in which the dependency is used. if (HttpRuntime.Cache.Get(rootCacheKey) != null) return; HttpRuntime.Cache.Add( rootCacheKey, rootCacheKey, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Default, null); } #endregion Methods } }
توضیحات کدهای فوق
در اینجا یک متدالحاقی به نام Cacheable توسعه داده شده است که میتواند در انتهای کوئریهای LINQ شما قرار گیرد. مثلا:
var data = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault());
کاری که در این متد انجام میشود به این شرح است:
الف) ابتدا کلید منحصربفرد معادل کوئری LINQ فراخوانی شده محاسبه میشود.
ب) بر اساس نام کامل نوع Entity در حال استفاده، کلید دیگری به نام rootCacheKey تولید میگردد.
شاید بپرسید اهمیت این کلید چیست؟
فرض کنید در حال حاضر 1000 آیتم در کش وجود دارند. چه روشی را برای حذف آیتمهای مرتبط با کش Entity1 پیشنهاد میدهید؟ احتمالا خواهید گفت تمام کش را بررسی کرده و آیتمها را یکی یکی حذف میکنیم.
این روش بسیار کند است (و جواب هم نمیدهد؛ چون کلیدی که در اینجا تولید شده، هش MD5 معادل کوئری است و نمیتوان آنرا به موجودیتی خاص ربط داد) و ... نکته جالبی در متد HttpRuntime.Cache.Insert برای مدیریت آن پیش بینی شده است: استفاده از CacheDependency.
توسط CacheDependency میتوان گروهی از آیتمهای همخانواده را تشکیل داد. سپس برای حذف کل این گروه کافی است کلید اصلی CacheDependency را حذف کرد. به این ترتیب به صورت خودکار کل کش مرتبط خالی میشود.
ج) مراحل بعدی آن هم یک سری اعمال متداول هستند. ابتدا توسط HttpRuntime.Cache.Get بررسی میشود که آیا بر اساس کلید متناظر با کوئری جاری، اطلاعاتی در کش وجود دارد یا خیر. اگر بله، نتیجه از کش خوانده میشود. اگر خیر، کوئری اصطلاحا materialized میشود تا بر روی بانک اطلاعاتی اجرا شده و نتیجه بازگشت داده شود. سپس این نتیجه را در کش قرار میدهیم.
مورد بعدی که باید به آن دقت داشت، خالی کردن کش، پس از به روز رسانی اطلاعات توسط کاربران است. این کار در متد InvalidateSecondLevelCache صورت میگیرد. به کمک ChangeTracker میتوان نام نوعهای موجودیتهای تغییر کرده را یافت. چون کلید اصلی CacheDependency را بر مبنای همین نام نوعهای موجودیتها تعیین کردهایم، به سادگی میتوان کش مرتبط با موجودیت یافت شده را خالی کرد.
استفاده از متد InvalidateSecondLevelCache یاد شده به نحو زیر است:
using System.Data.Entity; using EfSecondLevelCaching.Core; using EfSecondLevelCaching.Test.Models; namespace EfSecondLevelCaching.Test.DataLayer { public class ProductContext : DbContext { public DbSet<Product> Products { get; set; } public override int SaveChanges() { this.InvalidateSecondLevelCache(); return base.SaveChanges(); } } }
در اینجا با تحریف متد SaveChanges، میتوان درست در زمان اعمال تغییرات به بانک اطلاعاتی، قسمتی از کش را غیرمعتبر کرد.
نحوه استفاده از سطح دوم کش توسعه داده شده
مثالی از کاربرد متدهای الحاقی توسعه داده شده را در ذیل مشاهده میکنید:
using System.Data.Entity; using System.Linq; using EfSecondLevelCaching.Core; using EfSecondLevelCaching.Test.DataLayer; using EfSecondLevelCaching.Test.Models; using System; namespace EfSecondLevelCaching { public static class TestUsages { public static void RunQueries() { using (ProductContext context = new ProductContext()) { var isActive = true; var name = "Product1"; // reading from db var list1 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from cache var list2 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from cache var list3 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from db var list4 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == "Product2") .ToCacheableList(); } // removes products cache using (ProductContext context = new ProductContext()) { var p = new Product() { IsActive = false, ProductName = "P4", ProductNumber = "004" }; context.Products.Add(p); context.SaveChanges(); } using (ProductContext context = new ProductContext()) { var data = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault()); var data2 = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault()); context.SaveChanges(); } } } }
در این حالت اگر برنامه را اجرا کنیم به یک چنین خروجی در پنجره Debug ویژوال استودیو خواهیم رسید:
Adding new data: queryKey=72AF5DA1BA9B91E24DCCF83E88AD1C5F, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__72AF5DA1BA9B91E24DCCF83E88AD1C5F' from the cache. Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__72AF5DA1BA9B91E24DCCF83E88AD1C5F' from the cache. Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Adding new data: queryKey=11A2C33F9AD7821A0A31003BFF1DF886, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F queryKey: 11A2C33F9AD7821A0A31003BFF1DF886 queryKey: EfSecondLevelCaching.Test.Models.Product count: 3 Removing items with dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: count: 0 Available cached keys list: count: 0 Adding new data: queryKey=02E6FE403B461E45C5508684156C1D10, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: 02E6FE403B461E45C5508684156C1D10 queryKey: EfSecondLevelCaching.Test.Models.Product count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__02E6FE403B461E45C5508684156C1D10' from the cache.
توضیحات:
در زمان تولید list1 چون اطلاعاتی در کش سطح دوم وجود ندارد، پیغام Adding new data قابل مشاهده است. اطلاعات از بانک اطلاعاتی دریافت شده و سپس در کش قرار داده میشود.
حین فراخوانی list2 که دقیقا همان کوئری list1 را یکبار دیگر فراخوانی میکند، به عبارت Fetching object خواهیم رسید که بر دریافت اطلاعات از کش سطح دوم بجای مراجعه به بانک اطلاعاتی دلالت دارد.
در list4 چون پارامترهای کوئری تغییر کردهاند، بنابراین دیگر کلید منحصربفرد معادل آن با list1 و lis2 یکی نیست و اینبار پیغام Adding new data مشاهده میشود؛ چون برای دریافت اطلاعات آن نیاز است که به بانک اطلاعاتی مراجعه شود.
در ادامه یک context دیگر باز شده و در آن رکوردی به بانک اطلاعاتی اضافه میشود. به همین دلیل اینبار پیام Removing items with dependencyKey قابل مشاهده است. به عبارتی متد InvalidateSecondLevelCache وارد عمل شده است و بر اساس تغییری که صورت گرفته، کش را غیرمعتبر کرده است.
سپس در context بعدی تعریف شده، دوبار متد FirstOrDefault فراخوانی شده است. اولین مورد Adding new data است و دومین فراخوانی به Fetching object ختم شده است (دریافت اطلاعات از کش).
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید:
EfSecondLevelCaching.zip
از آنجائیکه اینترفیسها به معنای نوعهای سفارشی هستند و جاوا اسکریپت از آنها پشتیبانی نمیکند، توسط کامپایلر TypeScript، به هیچ نوع کد معادلی در جاوا اسکریپت، ترجمه و تبدیل نخواهند شد. کامپایلر TypeScript تنها از آنها جهت بررسی نوعها استفاده میکند.
اینترفیسها به صورت مجموعهای از تعاریف خواص و متدها، بدون پیاده سازی آنها تعریف میشوند. پیاده سازی این اینترفیسها، توسط کلاسها و یا سایر اشیاء صورت خواهند گرفت. برای مثال یک قرارداد اجاره، مشخص میکند که آخر هر ماه چه مقداری را باید پرداخت کرد. اما این قرار داد مشخص نمیکند که چگونه باید این پرداخت صورت گیرد و از هر شخصی به شخص دیگری میتواند متفاوت باشد. به این حالت duck typing هم میگویند. به این معنا که قرار داد، شکل یک شیء را مشخص میکند و تا زمانیکه پیاده سازی کنندهی آن بتواند این قرارداد را تامین کند، میتواند بجای نوع اصلی نیز بکار گرفته شود.
Duck typing چیست؟
duck typing به این معنا است که اگر پرندهای بتواند مانند یک اردک راه برود، شنا کند و صدا در بیاورد، یک اردک نامیده میشود. بنابراین همینقدر که یک شیء بتواند قراردادی را پیاده سازی کند، نوع آن با نوع اینترفیس یکی درنظر گرفته میشود. برای نمونه به مثال ذیل دقت کنید:
interface Duck { walk: () => void; swim: () => void; quack: () => void; } let probablyADuck = { walk: () => console.log('walking like a duck'), swim: () => console.log('swimming like a duck'), quack: () => console.log('quacking like a duck') } function FlyOverWater(bird: Duck) { } FlyOverWater(probablyADuck); // works!
در ادامه متغیر و شیءایی بدون تعریف نوع آن ایجاد شدهاست که همان متدهای اینترفیس Duck را پیاده سازی میکند و امضای آنها با امضای متدهای اینترفیس Duck یکی هستند.
سپس متد FlyOverWater تعریف شده که در آن، نوع پارامتر ورودی آن به صورت صریحی به نوع اینترفیس Duck مقید شدهاست.
در سطر بعدی، این متد با دریافت شیء probablyADuck فراخوانی شدهاست و چون این شیء تمام اجزای قرارداد Duck را پیاده سازی کردهاست، مشکلی در اجرای آن نخواهد بود. به این حالت duck typing میگویند.
نحوهی تعریف یک اینترفیس در TypeScript
تعریف یک اینترفیس با واژهی کلیدی interface شروع شده و سپس خواص و متدهای مدنظر این قرارداد، به همراه نوع آنها تعریف خواهند شد:
interface Book { id: number; title: string; author: string; pages?: number; markDamaged: (reason: string) => void; }
در اینترفیسهای TypeScript میتوان خواص اختیاری و optional را نیز تعریف کرد. نمونهی آن خاصیت pages در این مثال است که با ? مشخص شدهاست و نمونهی آنرا در حین تعریف پارامترهای اختیاری متدها نیز پیشتر ملاحظه کرده بودید.
تعریف متدها در یک اینترفیس، با مشخص سازی نام آن متد و ذکر یک کولن و سپس مشخص سازی امضای پارامترهای دریافتی انجام میشود. نوع خروجی متد، در سمت راست علامت <= قرار خواهد گرفت.
استفاده از اینترفیسها برای تعریف نوع خروجی توابع
در مثال زیر، متد CreateCustomerID دارای دو پارامتر ورودی از نوعهای رشتهای و عددی است و خروجی آن نیز از نوع رشتهای تعریف شدهاست:
function CreateCustomerID(name: string, id: number): string { return name + id; }
let IdGenerator: (chars: string, nums: number) => string;
IdGenerator = CreateCustomerID;
interface StringGenerator { (chars: string, nums: number): string; }
اکنون میتوان نحوهی تعریف متغیر IdGenerator را به صورت زیر Refactor کرد و تغییر داد:
let IdGenerator: StringGenerator;
بسط و توسعهی اینترفیسها
بسط و توسعهی اینترفیسها شبیه به مباحث ارث بری هستند. به این ترتیب که با بسط یک اینترفیس از طریق اینترفیسی دیگر، میتوان به نوعی مرکب رسید:
interface LibraryResource { catalogNumber: number; } interface LibraryBook { title: string; } interface Encyclopedia extends LibraryResource, LibraryBook { volume: number; }
این نوع مرکب، علاوه بر دارا بودن خاصیت volume مختص به خودش، اکنون حاوی دو خاصیت موجود در سایر اینترفیسهای ذکر شدهی در قسمت extends نیز هست.
حال اگر متغیر جدیدی را از نوع Encyclopedia تعریف کنیم، جهت برآورده شده تمام اجزای قرارداد، لازم است هر سه خاصیت را مقدار دهی نمائیم:
let refBook: Encyclopedia = { catalogNumber: 1234, title: 'The Book of Everything', volume: 1 }
نوع کلاسها
مبحث کلاسها به صورت جداگانهای در این سری بررسی خواهند شد. اما جهت تکمیل بحث جاری نیاز است اشارهی کوتاهی به آنها شود.
همانطور که عنوان شد، اینترفیسها تنها شکل و قرارداد پیاده سازی یک شیء را تعریف میکنند؛ بدون ارائهی پیاده سازی خاصی از آنها. تا اینجا در بحث جاری، اشیاء را توسط object literals داخل {} تعریف کردیم (مانند متغیر refBook مثال قبل). اما کلاسها روش بهتری برای انجام اینکار و تعریف اشیاء هستند.
در ذیل تعریف اینترفیس کتابدار را با تک متد doWork آن ملاحظه میکنید:
interface Librarian { doWork: () => void; }
class ElementarySchoolLibrarian implements Librarian { doWork() { console.log('Reading to and teaching children...'); } }
در ادامه برای ایجاد شیءایی از روی این تعریف، به نحو ذیل عمل میکنیم:
let kidsLibrarian: Librarian = new ElementarySchoolLibrarian(); kidsLibrarian.doWork();