اشتباهات متداول برنامهنویسهای دات نت
ممنون از نظرتان. آیا شما فید را در گوگل ریدر یا هر خوراک خوان دیگر هم ملاحظه کرده اید؟ حرف شما درست که بصورت استاندارد باید لینک و تایتل جدا باشند اما هدف مشاهده در خوراک خوان است که گمان میکنم با روش موجود برتری های بیشتری هم بر فید مرسوم داشته باشد.
بلاگر و دومین سفارشی
سلام؛ نوروز مبارک!
امیدوارم در این سال جدید برقرار به مانند سیسکو، محبوب مثل گوگل، پایدار همانند اینتل، به دور از گزند مثل ویندوز 7 و موفق همانند مایکروسافت باشید!
یک دامنهی جدید به نام dotnettips.info برای وبلاگ تهیه و تنظیم کردم که شرح انجام آن برای علاقمندان به صورت زیر است:
الف) خرید یک دامنه (برای مثال من از اینجا تهیه کردم)
ب) تنظیمات CNAME و A Records آن دامنه
برای مثال ممکن است کنترل پنل دامنهی شما به صورت زیر باشد. در این حالت به قسمت DNS مراجعه کرده:
و در ادامه تغییرات ذیل را اعمال نمائید:
در قسمت A Records چهار آدرس IP مربوط به گوگل باید وارد شوند:
4 رکورد جدید را باید ایجاد کنید:
سپس CNAME مربوط به بلاگر باید وارد شود:
ج) اکنون به تنظیمات بلاگ خود مراجعه کرده و نام دامنه جدید را وارد نمائید:
اعمال این تغییرات بین 7 ساعت تا 24 ساعت طول خواهد کشید (مدتی طول میکشد تا گوگل آدرس شما را فعال کند؛ مدتی هم طول خواهد کشید تا اطلاعات DNS شما در سراسر سرورهای مرتبط در دنیا به روز شود). بنابراین در این بین، انجام این تنظیمات و رها کردن سایت به حال خود کفایت میکند.
ماخذ IP ها و همچنین CNAME فوق مستندات رسمی گوگل است: (+)
اگر هم برای سایت خود یک فید از نوع feedburner ثبت کرده باشید، خوانندگان فید وبلاگ شما نیاز به هیچ نوع تغییر و تنظیمی نخواهند داشت.
چند وقت پیش کلا feeds.feedburner.com به همراه هزاران هزار زیر دامنهی آن فیلتر شدند؛ از دیروز هم feedproxy.google.com فیلتر شده و هزاران هزار زیر دامنهی آن دیگر به سادگی در دسترس نیستند. مورد اول را کسی زیاد جدی نگرفت چون لینک و راه حل سادهی دومی هم دارد، اما این مورد اخیر استفاده از گوگل ریدر را با مشکل مواجه کرده. در هر حال دلخوشی و مهمترین منابع مطالعاتی ما هم همین چیزها است ...
سایت آموزشی ZetCode
به نقل از این سایت: " مأموریت این سایت آموزش سریع و آسان و فهم تکنولوژیهای روز است. "
EF Code First #1
در ادامه بحث ASP.NET MVC میشود به ابزاری به نام MVC Scaffolding اشاره کرد. کار این ابزار که توسط یکی از اعضای تیم ASP.NET MVC به نام استیو اندرسون تهیه شده، تولید کدهای اولیه یک برنامه کامل ASP.NET MVC از روی مدلهای شما میباشد. حجم بالایی از کدهای تکراری آغازین برنامه را میشود توسط این ابزار تولید و بعد سفارشی کرد. MVC Scaffolding حتی قابلیت تولید کد بر اساس الگوی Repository و یا نوشتن Unit tests مرتبط را نیز دارد. بدیهی است این ابزار جای یک برنامه نویس را نمیتواند پر کند اما کدهای آغازین یک سری کارهای متداول و تکراری را به خوبی میتواند پیاده سازی و ارائه دهد. زیر ساخت این ابزار، علاوه بر ASP.NET MVC، آشنایی با Entity framework code first است.
در طی سری ASP.NET MVC که در این سایت تا به اینجا مطالعه کردید من به شدت سعی کردم از ابزارگرایی پرهیز کنم. چون شخصی که نمیداند مسیریابی چیست، اطلاعات چگونه به یک کنترلر منتقل یا به یک View ارسال میشوند، قراردادهای پیش فرض فریم ورک چیست یا زیر ساخت امنیتی یا فیلترهای ASP.NET MVC کدامند، چطور میتواند از ابزار پیشرفته Code generator ایی استفاده کند، یا حتی در ادامه کدهای تولیدی آنرا سفارشی سازی کند؟ بنابراین برای استفاده از این ابزار و درک کدهای تولیدی آن، نیاز به یک پیشنیاز دیگر هم وجود دارد: «Entity framework code first»
امسال دو کتاب خوب در این زمینه منتشر شدهاند به نامهای:
Programming Entity Framework: DbContext, ISBN: 978-1-449-31296-1
Programming Entity Framework: Code First, ISBN: 978-1-449-31294-7
که هر دو به صورت اختصاصی به مقوله EF Code first پرداختهاند.
در طی روزهای بعدی EF Code first را با هم مرور خواهیم کرد و البته این مرور مستقل است از نوع فناوری میزبان آن؛ میخواهد یک برنامه کنسول باشد یا WPF یا یک سرویس ویندوز NT و یا ... یک برنامه وب.
البته از دیدگاه مایکروسافت، M در MVC به معنای EF Code first است. به همین جهت MVC3 به صورت پیش فرض ارجاعی را به اسمبلیهای آن دارد و یا حتی به روز رسانی که برای آن ارائه داده نیز در جهت تکمیل همین بحث است.
مروری سریع بر تاریخچه Entity framework code first
ویژوال استودیو 2010 و دات نت 4، به همراه EF 4.0 ارائه شدند. با این نگارش امکان استفاده از حالتهای طراحی database first و model first مهیا است. پس از آن، به روز رسانیهای EF خارج از نوبت و به صورت منظم، هر از چندگاهی ارائه میشوند و در زمان نگارش این مطلب، آخرین نگارش پایدار در دسترس آن 4.3.1 میباشد. از زمان EF 4.1 به بعد، نوع جدیدی از مدل سازی به نام Code first به این فریم ورک اضافه شد و در نگارشهای بعدی آن، مباحث DB migration جهت ساده سازی تطابق اطلاعات مدلها با بانک اطلاعاتی، اضافه گردیدند. در روش Code first، کار با طراحی کلاسها که در اینجا مدل دادهها نامیده میشوند، شروع گردیده و سپس بر اساس این اطلاعات، تولید یک بانک اطلاعاتی جدید و یا استفاده از نمونهای موجود میسر میگردد.
پیشتر در روش database first ابتدا یک بانک اطلاعاتی موجود، مهندسی معکوس میشد و از روی آن فایل XML ایی با پسوند EDMX تولید میگشت. سپس به کمک entity data model designer ویژوال استودیو، این فایل نمایش داده شده و یا امکان اعمال تغییرات بر روی آن میسر میشد. همچنین در روش دیگری به نام model first نیز کار از entity data model designer جهت طراحی موجودیتها آغاز میگشت.
اما با روش Code first دیگر در ابتدای امر مدل فیزیکی و یک بانک اطلاعاتی وجود خارجی ندارد. در اینجا EF تعاریف کلاسهای شما را بررسی کرده و بر اساس آن، اطلاعات نگاشتهای خواص کلاسها به جداول و فیلدهای بانک اطلاعاتی را تشکیل میدهد. البته عموما تعاریف ساده کلاسها بر این منظور کافی نیستند. به همین جهت از یک سری متادیتا به نام ویژگیها یا اصطلاحا data annotations مهیا در فضای نام System.ComponentModel.DataAnnotations برای افزودن اطلاعات لازم مانند نام فیلدها، جداول و یا تعاریف روابط ویژه نیز استفاده میگردد. به علاوه در روش Code first یک API جدید به نام Fluent API نیز جهت تعاریف این ویژگیها و روابط، با کدنویسی مستقیم نیز درنظر گرفته شده است. نهایتا از این اطلاعات جهت نگاشت کلاسها به بانک اطلاعاتی و یا برای تولید ساختار یک بانک اطلاعاتی خالی جدید نیز میتوان کمک گرفت.
مزایای EF Code first
- مطلوب برنامه نویسها! : برنامه نویسهایی که مدتی تجربه کار با ابزارهای طراح را داشته باشند به خوبی میدانند این نوع ابزارها عموما demo-ware هستند. چندجا کلیک میکنید، دوبار Next، سه بار OK و ... به نظر میرسد کار تمام شده. اما واقعیت این است که عمری را باید صرف نگهداری و یا پیاده سازی جزئیاتی کرد که انجام آنها با کدنویسی مستقیم بسیار سریعتر، سادهتر و با کنترل بیشتری قابل انجام است.
- سرعت: برای کار با EF Code first نیازی نیست در ابتدای کار بانک اطلاعاتی خاصی وجود داشته باشد. کلاسهای خود را طراحی و شروع به کدنویسی کنید.
- سادگی: در اینجا دیگر از فایلهای EDMX خبری نیست و نیازی نیست مرتبا آنها را به روز کرده یا نگهداری کرد. تمام کارها را با کدنویسی و کنترل بیشتری میتوان انجام داد. به علاوه کنترل کاملی بر روی کد نهایی تهیه شده نیز وجود دارد و توسط ابزارهای تولید کد، ایجاد نمیشوند.
- طراحی بهتر بانک اطلاعاتی نهایی: اگر طرح دقیقی از مدلهای برنامه داشته باشیم، میتوان آنها را به المانهای کوچک و مشخصی، تقسیم و refactor کرد. همین مساله در نهایت مباحث database normalization را به نحوی مطلوب و با سرعت بیشتری میسر میکند.
- امکان استفاده مجدد از طراحی کلاسهای انجام شده در سایر ORMهای دیگر. چون طراحی مدلهای برنامه به بانک اطلاعاتی خاصی گره نمیخورند و همچنین الزاما هم قرار نیست جزئیات کاری EF در آنها لحاظ شود، این کلاسها در صورت نیاز در سایر پروژهها نیز به سادگی قابل استفاده هستند.
- ردیابی سادهتر تغییرات: روش اصولی کار با پروژههای نرم افزاری همواره شامل استفاده از یک ابزار سورس کنترل مانند SVN، Git، مرکوریال و امثال آن است. به این ترتیب ردیابی تغییرات انجام شده به سادگی توسط این ابزارها میسر میشوند.
- سادهتر شدن طراحیهای پیچیدهتر: برای مثال پیاده سازی ارث بری، ایجاد کلاسهای خود ارجاع دهنده و امثال آن با کدنویسی سادهتر است.
دریافت آخرین نگارش EF
برای دریافت و نصب آخرین نگارش EF نیاز است از NuGet استفاده شود و این مزایا را به همراه دارد:
به کمک NuGet امکان با خبر شدن از به روز رسانی جدید صورت گرفته به صورت خودکار درنظر گرفته شده است و همچنین کار دریافت بستههای مرتبط و به روز رسانی ارجاعات نیز در این حالت خودکار است. به علاوه توسط NuGet امکان دسترسی به کتابخانههایی که مثلا در گوگلکد قرار دارند و به صورت معمول امکان دریافت آنها برای ما میسر نیست، نیز بدون مشکل فراهم است (برای نمونه ELMAH، که اصل آن از گوگلکد قابل دریافت است؛ اما بسته نیوگت آن نیز در دسترس میباشد).
پس از نصب NuGet، تنها کافی است بر روی گره References در Solution explorer ویژوال استودیو، کلیک راست کرده و به کمک NuGet آخرین نگارش EF را نصب کرد. در گالری آنلاین آن، عموما EF اولین گزینه است (به علت تعداد بالای دریافت آن).
حین استفاده از NuGet جهت نصب Ef، ابتدا ارجاعاتی به اسمبلیهای زیر به برنامه اضافه خواهند شد:
System.ComponentModel.DataAnnotations.dll
System.Data.Entity.dll
EntityFramework.dll
بدیهی است بدون استفاده از NuGet، تمام این کارها را باید دستی انجام داد.
سپس در پوشهای به نام packages، فایلهای مرتبط با EF قرار خواهند گرفت که شامل اسمبلی آن به همراه ابزارهای DB Migration است. همچنین فایل packages.config که شامل تعاریف اسمبلیهای نصب شده است به پروژه اضافه میشود. NuGet به کمک این فایل و شماره نگارش درج شده در آن، شما را از به روز رسانیهای بعدی مطلع خواهد ساخت:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="4.3.1" />
</packages>
همچنین اگر به فایل app.config یا web.config برنامه نیز مراجعه کنید، یک سری تنظیمات ابتدایی اتصال به بانک اطلاعاتی در آن ذکر شده است:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</configSections>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework">
<parameters>
<parameter value="Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=True" />
</parameters>
</defaultConnectionFactory>
</entityFramework>
</configuration>
همانطور که ملاحظه میکنید بانک اطلاعاتی پیش فرضی که در اینجا ذکر شده است، LocalDB میباشد. این بانک اطلاعاتی را از این آدرس نیز میتوانید دریافت کنید.
البته ضرورتی هم به استفاده از آن نیست و از سایر نگارشهای SQL Server نیز میتوان استفاده کرد ولی خوب ... مزیت استفاده از آن برای کاربر نهایی این است که «نیازی به یک مهندس برای نصب، راه اندازی و نگهداری ندارد». تنها مشکل آن این است که از ویندوز XP پشتیبانی نمیکند. البته SQL Server CE 4.0 این محدودیت را ندارد.
ضمن اینکه باید درنظر داشت EF به فناوری میزبان خاصی گره نخورده است و مثالهایی که در اینجا بررسی میشوند صرفا تعدادی برنامه کنسول معمولی هستند و نکات عنوان شده در آنها در تمام فناوریهای میزبان موجود به یک نحو کاربرد دارند.
قراردادهای پیش فرض EF Code first
عنوان شد که اطلاعات کلاسهای ساده تشکیل دهنده مدلهای برنامه، برای تعریف جداول و فیلدهای یک بانک اطلاعات و همچنین مشخص سازی روابط بین آنها کافی نیستند و مرسوم است برای پر کردن این خلاء از یک سری متادیتا و یا Fluent API مهیا نیز استفاده گردد. اما در EF Code first یک سری قرار داد توکار نیز وجود دارند که مطلع بودن از آنها سبب خواهد شد تا حجم کدنویسی و تنظیمات جانبی این فریم ورک به حداقل برسند. برای نمونه مدلهای معروف بلاگ و مطالب آنرا درنظر بگیرید:
namespace EF_Sample01.Models
{
public class Post
{
public int Id { set; get; }
public string Title { set; get; }
public string Content { set; get; }
public virtual Blog Blog { set; get; }
}
}
using System.Collections.Generic;
namespace EF_Sample01.Models
{
public class Blog
{
public int Id { set; get; }
public string Title { set; get; }
public string AuthorName { set; get; }
public IList<Post> Posts { set; get; }
}
}
یکی از قراردادهای EF Code first این است که کلاسهای مدل شما را جهت یافتن خاصیتی به نام Id یا ClassId مانند BlogId، جستجو میکند و از آن به عنوان primary key و فیلد identity جدول بانک اطلاعاتی استفاده خواهد کرد.
همچنین در کلاس Blog، خاصیت لیستی از Posts و در کلاس Post خاصیت virtual ایی به نام Blog وجود دارند. به این ترتیب روابط بین دو کلاس و ایجاد کلید خارجی متناظر با آنرا به صورت خودکار انجام خواهد داد.
نهایتا از این اطلاعات جهت تشکیل database schema یا ساختار بانک اطلاعاتی استفاده میگردد.
اگر به فضاهای نام دو کلاس فوق دقت کرده باشید، به کلمه Models ختم شدهاند. به این معنا که در پوشهای به همین نام در پروژه جاری قرار دارند. یا مرسوم است کلاسهای مدل را در یک پروژه class library مجزا به نام DomainClasses نیز قرار دهند. این پروژه نیازی به ارجاعات اسمبلیهای EF ندارد و تنها به اسمبلی System.ComponentModel.DataAnnotations.dll نیاز خواهد داشت.
EF Code first چگونه کلاسهای مورد نظر را انتخاب میکند؟
ممکن است دهها و صدها کلاس در یک پروژه وجود داشته باشند. EF Code first چگونه از بین این کلاسها تشخیص خواهد داد که باید از کدامیک استفاده کند؟ اینجا است که مفهوم جدیدی به نام DbContext معرفی شده است. برای تعریف آن یک کلاس دیگر را به پروژه برای مثال به نام Context اضافه کنید. همچنین مرسوم است که این کلاس را در پروژه class library دیگری به نام DataLayer اضافه میکنند. این پروژه نیاز به ارجاعی به اسمبلیهای EF خواهد داشت. در ادامه کلاس جدید اضافه شده باید از کلاس DbContext مشتق شود:
using System.Data.Entity;
using EF_Sample01.Models;
namespace EF_Sample01
{
public class Context : DbContext
{
public DbSet<Blog> Blogs { set; get; }
public DbSet<Post> Posts { set; get; }
}
}
سپس در اینجا به کمک نوع جنریکی به نام DbSet، کلاسهای دومین برنامه را معرفی میکنیم. به این ترتیب، EF Code first ابتدا به دنبال کلاسی مشتق شده از DbContext خواهد گشت. پس از یافتن آن، خواصی از نوع DbSet را بررسی کرده و نوعهای متناظر با آنرا به عنوان کلاسهای دومین درنظر میگیرد و از سایر کلاسهای برنامه صرفنظر خواهد کرد. این کل کاری است که باید انجام شود.
اگر دقت کرده باشید، نام کلاسهای موجودیتها، مفرد هستند و نام خواص تعریف شده به کمک DbSet، جمع میباشند که نهایتا متناظر خواهند بود با نام جداول بانک اطلاعاتی تشکیل شده.
تشکیل خودکار بانک اطلاعاتی و افزودن اطلاعات به جداول
تا اینجا بدون تهیه یک بانک اطلاعاتی نیز میتوان از کلاس Context تهیه شده استفاده کرد و کار کدنویسی را آغاز نمود. بدیهی است جهت اجرای نهایی کدها، نیاز به یک بانک اطلاعاتی خواهد بود. اگر تنظیمات پیش فرض فایل کانفیگ برنامه را تغییر ندهیم، از همان defaultConnectionFactory یاده شده استفاده خواهد کرد. در این حالت نام بانک اطلاعاتی به صورت خودکار تنظیم شده و مساوی «EF_Sample01.Context» خواهد بود.
برای سفارشی سازی آن نیاز است فایل app.config یا web.config برنامه را اندکی ویرایش نمود:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
...
</configSections>
<connectionStrings>
<clear/>
<add name="Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true"
providerName="System.Data.SqlClient"
/>
</connectionStrings>
...
</configuration>
در اینجا به بانک اطلاعاتی testdb2012 در وهله پیش فرض SQL Server نصب شده، اشاره شده است. فقط باید دقت داشت که تگ configSections باید در ابتدای فایل قرار گیرد و مابقی تنظیمات پس از آن.
یا اگر علاقمند باشید که از SQL Server CE استفاده کنید، تنظیمات رشته اتصالی را به نحو زیر مقدار دهی نمائید:
<connectionStrings>
<add name="MyContextName"
connectionString="Data Source=|DataDirectory|\Store.sdf"
providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>
در هر دو حالت، name باید به نام کلاس مشتق شده از DbContext اشاره کند که در مثال جاری همان Context است.
یا اگر علاقمند بودید که این قرارداد توکار را تغییر داده و نام رشته اتصالی را با کدنویسی تعیین کنید، میتوان به نحو زیر عمل کرد:
public class Context : DbContext
{
public Context()
: base("ConnectionStringName")
{
}
البته ضرورتی ندارد این بانک اطلاعاتی از پیش موجود باشد. در اولین بار اجرای کدهای زیر، به صورت خودکار بانک اطلاعاتی و جداول Blogs و Posts و روابط بین آنها تشکیل میگردد:
using EF_Sample01.Models;
namespace EF_Sample01
{
class Program
{
static void Main(string[] args)
{
using (var db = new Context())
{
db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
db.SaveChanges();
}
}
}
}
در این تصویر چند نکته حائز اهمیت هستند:
الف) نام پیش فرض بانک اطلاعاتی که به آن اشاره شد (اگر تنظیمات رشته اتصالی قید نگردد).
ب) تشکیل خودکار primary key از روی خواصی به نام Id
ج) تشکیل خودکار روابط بین جداول و ایجاد کلید خارجی (به کمک خاصیت virtual تعریف شده)
د) تشکیل جدول سیستمی به نام dbo.__MigrationHistory که از آن برای نگهداری سابقه به روز رسانیهای ساختار جداول کمک گرفته خواهد شد.
ه) نوع و طول فیلدهای متنی، nvarchar از نوع max است.
تمام اینها بر اساس پیش فرضها و قراردادهای توکار EF Code first انجام شده است.
در کدهای تعریف شده نیز، ابتدا یک وهله از شیء Context ایجاد شده و سپس به کمک آن میتوان به جدول Blogs اطلاعاتی را افزود و در آخر ذخیره نمود. استفاده از using هم دراینجا نباید فراموش شود، زیرا اگر استثنایی در این بین رخ دهد، کار پاکسازی منابع و بستن اتصال گشوده شده به بانک اطلاعاتی به صورت خودکار انجام خواهد شد.
در ادامه اگر بخواهیم مطلبی را به Blog ثبت شده اضافه کنیم، خواهیم داشت:
using EF_Sample01.Models;
namespace EF_Sample01
{
class Program
{
static void Main(string[] args)
{
//addBlog();
addPost();
}
private static void addPost()
{
using (var db = new Context())
{
var blog = db.Blogs.Find(1);
db.Posts.Add(new Post
{
Blog = blog,
Content = "data",
Title = "EF"
});
db.SaveChanges();
}
}
private static void addBlog()
{
using (var db = new Context())
{
db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
db.SaveChanges();
}
}
}
}
متد db.Blogs.Find، بر اساس primary key بلاگ ثبت شده، یک وهله از آنرا یافته و سپس از آن جهت تشکیل شیء Post و افزودن آن به جدول Posts استفاده میشود. متد Find ابتدا Contxet جاری را جهت یافتن شیءایی با id مساوی یک جستجو میکند (اصطلاحا به آن first level cache هم گفته میشود). اگر موفق به یافتن آن شد، بدون صدور کوئری اضافهای به بانک اطلاعاتی از اطلاعات همان شیء استفاده خواهد کرد. در غیراینصورت نیاز خواهد داشت تا ابتدا کوئری لازم را به بانک اطلاعاتی ارسال کرده و اطلاعات شیء Blog متناظر با id=1 را دریافت کند. همچنین اگر نیاز داشتیم تا تنها با سطح اول کش کار کنیم، در EF Code first میتوان از خاصیتی به نام Local نیز استفاده کرد. برای مثال خاصیت db.Blogs.Local بیانگر اطلاعات موجود در سطح اول کش میباشد.
نهایتا کوئری Insert تولید شده توسط آن به شکل زیر خواهد بود (لاگ شده توسط برنامه SQL Server Profiler):
exec sp_executesql N'insert [dbo].[Posts]([Title], [Content], [Blog_Id])
values (@0, @1, @2)
select [Id]
from [dbo].[Posts]
where @@ROWCOUNT > 0 and [Id] = scope_identity()',
N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 int',
@0=N'EF',
@1=N'data',
@2=1
این نوع کوئرهای پارامتری چندین مزیت مهم را به همراه دارند:
الف) به صورت خودکار تشکیل میشوند. تمام کوئریهای پشت صحنه EF پارامتری هستند و نیازی نیست مرتبا مزایای این امر را گوشزد کرد و باز هم عدهای با جمع زدن رشتهها نسبت به نوشتن کوئریهای نا امن SQL اقدام کنند.
ب) کوئرهای پارامتری در مقابل حملات تزریق اس کیوال مقاوم هستند.
ج) SQL Server با کوئریهای پارامتری همانند رویههای ذخیره شده رفتار میکند. یعنی query execution plan محاسبه شده آنها را کش خواهد کرد. همین امر سبب بالا رفتن کارآیی برنامه در فراخوانیهای بعدی میگردد. الگوی کلی مشخص است. فقط پارامترهای آن تغییر میکنند.
د) مصرف حافظه SQL Server کاهش مییابد. چون SQL Server مجبور نیست به ازای هر کوئری اصطلاحا Ad Hoc رسیده یکبار execution plan متفاوت آنها را محاسبه و سپس کش کند. این مورد مشکل مهم تمام برنامههایی است که از کوئریهای پارامتری استفاده نمیکنند؛ تا حدی که گاهی تصور میکنند شاید SQL Server دچار نشتی حافظه شده، اما مشکل جای دیگری است.
مشکل! ساختار بانک اطلاعاتی تشکیل شده مطلوب کار ما نیست.
تا همینجا با حداقل کدنویسی و تنظیمات مرتبط با آن، پیشرفت خوبی داشتهایم؛ اما نتیجه حاصل آنچنان مطلوب نیست و نیاز به سفارشی سازی دارد. برای مثال طول فیلدها را نیاز داریم به مقدار دیگری تنظیم کنیم، تعدادی از فیلدها باید به صورت not null تعریف شوند یا نام پیش فرض بانک اطلاعاتی باید مشخص گردد و مواردی از این دست. با این موارد در قسمتهای بعدی بیشتر آشنا خواهیم شد.
ایجاد برنامههای ابتدایی مورد نیاز
در ابتدا دو پوشهی جدید BlazorServerApp و WinFormsApp را ایجاد میکنیم. سپس از طریق خط فرمان در اولی دستور dotnet new blazorserver و در دومی دستور dotnet new winforms را اجرا میکنیم تا دو برنامهی خالی Blazor Server و همچنین Windows Forms، ایجاد شوند. برنامهی WinForms ایجاد شده مبتنی بر NET Core. و یا همان NET 6x. است؛ بجای اینکه مبتنی بر دات نت فریمورک 4x باشد.
ایجاد یک پروژهی کتابخانهی Razor
چون میخواهیم کدهای برنامهی BlazorServerApp ما در برنامهی WinForms قابل استفاده باشد، نیاز است فایلهای اصلی آنرا به یک پروژهی razor class library منتقل کنیم. به همین جهت برای این پروژه، یک پوشهی جدید را به نام BlazorClassLibrary ایجاد کرده و درون آن دستور dotnet new razorclasslib را اجرا میکنیم.
انتقال فایلهای پروژهی Blazor به پروژهی کتابخانهی Razor
در ادامه این فایلها را از پروژهی BlazorServerApp به پروژهی BlazorClassLibrary منتقل میکنیم:
- کل پوشهی Data
- کل پوشهی Pages
- کل پوشهی Shared
- فایل App.razor
- فایل Imports.razor_
- کل پوشهی wwwroot
پس از اینکار، نیاز است فایل csproj کتابخانهی class lib را اندکی ویرایش کرد تا بتواند فایلهای اضافه شده را کامپایل کند:
<Project Sdk="Microsoft.NET.Sdk.Razor"> <PropertyGroup> <AddRazorSupportForMvc>true</AddRazorSupportForMvc> </PropertyGroup> <ItemGroup> <FrameworkReference Include="Microsoft.AspNetCore.App" /> </ItemGroup> </Project>
- به علاوه فایل Error.cshtml.cs انتقالی، نیاز به افزودن فضای نام using Microsoft.Extensions.Logging را خواهد داشت.
- در فایل Imports.razor_ انتقالی نیاز است دو using آخر آنرا که به BlazorServerApp قبلی اشاره میکنند، به BlazorClassLibrary جدید ویرایش کنیم:
@using BlazorClassLibrary @using BlazorClassLibrary.Shared
@namespace BlazorClassLibrary.Pages
<link rel="stylesheet" href="_content/BlazorClassLibrary/css/bootstrap/bootstrap.min.css" /> <link href="_content/BlazorClassLibrary/css/site.css" rel="stylesheet" />
پس از این تغییرات، برای اینکه برنامهی BlazorServerApp موجود، به کار خود ادامه دهد، نیاز است ارجاعی از پروژهی class lib را به فایل csproj آن اضافه کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" /> </ItemGroup> </Project>
ویرایش برنامهی WinForms جهت اجرای کدهای Blazor
تا اینجا برنامهی Blazor Server ما تمام فایلهای مورد نیاز خود را از BlazorClassLibrary دریافت میکند و بدون مشکل اجرا میشود. در ادامه میخواهیم کار هاست این class lib را در برنامهی WinForms نیز انجام دهیم. به همین جهت در ابتدا ارجاعی را به class lib به آن اضافه میکنیم:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" /> </ItemGroup> </Project>
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="6.0.101-preview.11.2349" /> </ItemGroup> </Project>
در ادامه نیاز است فایل Form1.Designer.cs را به صورت دستی جهت افزودن این WebView اضافه شده، تغییر داد:
namespace WinFormsApp; partial class Form1 { private void InitializeComponent() { this.blazorWebView1 = new Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView(); this.SuspendLayout(); this.blazorWebView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.blazorWebView1.Location = new System.Drawing.Point(13, 181); this.blazorWebView1.Name = "blazorWebView1"; this.blazorWebView1.Size = new System.Drawing.Size(775, 257); this.blazorWebView1.TabIndex = 20; this.Controls.Add(this.blazorWebView1); this.components = new System.ComponentModel.Container(); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(800, 450); this.Text = "Form1"; this.ResumeLayout(false); } private Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView blazorWebView1; }
هاست برنامهی Blazor در برنامهی WinForm
پس از تغییرات فوق، نیاز است فایلهای wwwroot را از پروژهی class lib به پروژهی WinForms کپی کرد. از این جهت که این فایلها از طریق index.html جدیدی خوانده خواهند شد. پس از کپی کردن این پوشه، نیاز است فایل csproj پروژهی WinForm را به صورت زیر اصلاح کرد:
<Project Sdk="Microsoft.NET.Sdk.Razor"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="6.0.101-preview.11.2349" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" /> </ItemGroup> <ItemGroup> <Content Update="wwwroot\**"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> </ItemGroup> </Project>
در ادامه داخل این پوشهی wwwroot که از پروژهی class lib کپی کردیم، نیاز است فایل index.html جدیدی را که قرار است blazor.webview.js را اجرا کند، به صورت زیر ایجاد کنیم:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>Blazor WinForms app</title> <base href="/" /> <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> <link href="css/site.css" rel="stylesheet" /> <link href="css/app.css" rel="stylesheet" /> <link href="WinFormsApp.styles.css" rel="stylesheet" /> </head> <body> <div id="app"></div> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="">Reload</a> <a>🗙</a> </div> <script src="_framework/blazor.webview.js"></script> </body> </html>
- همچنین در این فایل باید مداخل css.های مورد نیاز را هم مجددا ذکر کرد.
مرحلهی آخر کار، استفاده از کامپوننت webview جهت نمایش فایل index.html فوق است:
using System; using System.Windows.Forms; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebView.WindowsForms; using Microsoft.Extensions.DependencyInjection; using BlazorServerApp.Data; using BlazorClassLibrary; namespace WinFormsApp; public partial class Form1 : Form { private readonly AppState _appState = new(); public Form1() { var serviceCollection = new ServiceCollection(); serviceCollection.AddBlazorWebView(); serviceCollection.AddSingleton<AppState>(_appState); serviceCollection.AddSingleton<WeatherForecastService>(); InitializeComponent(); blazorWebView1.HostPage = @"wwwroot\index.html"; blazorWebView1.Services = serviceCollection.BuildServiceProvider(); blazorWebView1.RootComponents.Add<App>("#app"); //blazorWebView1.Dock = DockStyle.Fill; } }
نکتهی مهم! حتما نیاز است WebView2 Runtime را جداگانه دریافت و نصب کرد. در غیر اینصورت در حین اجرای برنامه، با خطای نامفهوم زیر مواجه خواهید شد:
System.IO.FileNotFoundException: The system cannot find the file specified. (0x80070002)
در اینجا یک ServiceCollection را ایجاد کرده و توسط آن سرویسهای مورد نیاز کامپوننت WebView را تامین میکنیم. همچنین مسیر فایل index.html نیز توسط آن مشخص شدهاست. این تنظیمات شبیه به فایل Program.cs برنامهی Blazor هستند.
تا اینجا اگر برنامه را اجرا کنیم، چنین خروجی قابل مشاهدهاست:
اکنون برنامهی کامل Blazor Server ما توسط یک WinForms هاست شدهاست و کاربر برای کار با آن، نیاز به نصب IIS یا هیچ وب سرور خاصی ندارد.
تعامل بین برنامهی WinForm و برنامهی Blazor
میخواهیم یک دکمه را بر روی WinForm قرار داده و با کلیک بر روی آن، مقدار شمارشگر حاصل در برنامهی Blazor را نمایش دهیم؛ مانند تصویر فوق.
برای اینکار در کدهای فوق، ثبت سرویس جدید AppState را هم مشاهده میکنید:
serviceCollection.AddSingleton<AppState>(_appState);
namespace BlazorServerApp.Data; public class AppState { public int Counter { get; set; } }
builder.Services.AddSingleton<AppState>();
@inject BlazorServerApp.Data.AppState AppState // ... @code { private void IncrementCount() { // ... AppState.Counter++; } }
private void button1_Click(object sender, EventArgs e) { MessageBox.Show( owner: this, text: $"Current counter value is: {_appState.Counter}", caption: "Counter"); }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorDesktopHybrid.zip
EF Code First #11
ذکر کردید "بهتر است یک لایه ی اضافی به نام سرویس ایجاد شود و اعمال مربوط به EF را به آن منتقل نمود" یعنی به ازای هر موجودیت یک سرویس هم داشته باشیم ؟ این (http://pastebin.com/iUCn1303) نمونه کدی که اینجا قرار دادم صحیح است ؟ -Service همان Business logic ما خواهد بود ؟ یعنی علاوه بر اعمال CRUD ، بررسی منطق تجاری ، Validate کردن Entity و یا اعمال شرط های مختلف در Query ها در همین لایه ی سرویس انجام می شود ؟
-اگر بخواهیم اطلاعات User را با اطلاعات پروفایل در قالب یک لیست برای لایه ی پایین تر بفرستیم ، نوع خروجی چه باید باشد ؟ مثلا UserProfileDataTransferObject ؟
در سری هشتم Refactoring در مورد God Classes توضیح دادید. برای منطق های پیچیده روی یک Enity مثل User مثلا چندین Query با حالت های مختلف کلاس UserService به God Class تبدیل می شود. راه حل چیست ؟ اگر ممکن است پروژه ی کد باز مورد تایید خودتون برای مرور کد ها معرفی کنید.
خیلی ممنون
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
ارائهی AuthenticationState به تمام کامپوننتهای یک برنامهی Blazor WASM
در قسمت 22، با مفاهیم CascadingAuthenticationState و AuthorizeRouteView در برنامههای Blazor Server آشنا شدیم؛ این مفاهیم در اینجا نیز یکی هستند:
- کامپوننت CascadingAuthenticationState سبب میشود AuthenticationState (لیستی از Claims کاربر)، به تمام کامپوننتهای یک برنامهیBlazor ارسال شود. در مورد پارامترهای آبشاری، در قسمت نهم این سری بیشتر بحث شد و هدف از آن، ارائهی یکسری اطلاعات، به تمام زیر کامپوننتهای یک کامپوننت والد است؛ بدون اینکه نیاز باشد مدام این پارامترها را در هر زیر کامپوننتی، تعریف و تنظیم کنیم. همینقدر که آنها را در بالاترین سطح سلسله مراتب کامپوننتهای تعریف شده تعریف کردیم، در تمام زیر کامپوننتهای آن نیز در دسترس خواهند بود.
- کامپوننت AuthorizeRouteView امکان محدود کردن دسترسی به صفحات مختلف برنامهی Blazor را بر اساس وضعیت اعتبارسنجی و نقشهای کاربر جاری، میسر میکند.
روش اعمال این دو کامپوننت نیز یکی است و نیاز به ویرایش فایل BlazorWasm.Client\App.razor در اینجا وجود دارد:
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <Authorizing> <p>Please wait, we are authorizing the user.</p> </Authorizing> <NotAuthorized> <p>Not Authorized</p> </NotAuthorized> </AuthorizeRouteView> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
مشکل! برخلاف برنامههای Blazor Server، برنامههای Blazor WASM به صورت پیشفرض به همراه تامین کنندهی توکار AuthenticationState نیستند.
اگر سری Blazor جاری را از ابتدا دنبال کرده باشید، کاربرد AuthenticationState را در برنامههای Blazor Server، در قسمتهای 21 تا 23، پیشتر مشاهده کردهاید. همان مفاهیم، در برنامههای Blazor WASM هم قابل استفاده هستند؛ البته در اینجا به علت جدا بودن برنامهی سمت کلاینت WASM Blazor، از برنامهی Web API سمت سرور، نیاز است یک تامین کنندهی سمت کلاینت AuthenticationState را بر اساس JSON Web Token دریافتی از سرور، تشکیل دهیم و برخلاف برنامههای Blazor Server، این مورد به صورت خودکار مدیریت نمیشود و با ASP.NET Core Identity سمت سروری که JWT تولید میکند، یکپارچه نیست.
بنابراین در اینجا نیاز است یک AuthenticationStateProvider سفارشی سمت کلاینت را تهیه کنیم که بر اساس JWT دریافتی از Web API کار میکند. به همین جهت در ابتدا یک JWT Parser را طراحی میکنیم که رشتهی JWT دریافتی از سرور را تبدیل به <IEnumerable<Claim میکند. سپس این لیست را در اختیار یک AuthenticationStateProvider سفارشی قرار میدهیم تا اطلاعات مورد نیاز کامپوننتهای CascadingAuthenticationState و AuthorizeRouteView تامین شده و قابل استفاده شوند.
نیاز به یک JWT Parser
در قسمت 25، پس از لاگین موفق، یک JWT تولید میشود که به همراه قسمتی از مشخصات کاربر است. میتوان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که برای نمونه به این خروجی میرسیم و حاوی claims تعریف شدهاست:
{ "iss": "https://localhost:5001/", "iat": 1616396383, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir", "Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c", "DisplayName": "vahid@dntips.ir", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "nbf": 1616396383, "exp": 1616397583, "aud": "Any" }
using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Json; namespace BlazorWasm.Client.Utils { /// <summary> /// From the Steve Sanderson’s Mission Control project: /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs /// </summary> public static class JwtParser { public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt) { var claims = new List<Claim>(); var payload = jwt.Split('.')[1]; var jsonBytes = ParseBase64WithoutPadding(payload); var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes); claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()))); return claims; } private static byte[] ParseBase64WithoutPadding(string base64) { switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Convert.FromBase64String(base64); } } }
تامین AuthenticationState مبتنی بر JWT مخصوص برنامههای Blazor WASM
پس از داشتن لیست Claims دریافتی از یک رشتهی JWT، اکنون میتوان آنرا تبدیل به یک AuthenticationStateProvider کرد. برای اینکار در ابتدا نیاز است بستهی نیوگت Microsoft.AspNetCore.Components.Authorization را به برنامهی کلاینت اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0.4" /> </ItemGroup> </Project>
namespace BlazorWasm.Client.Services { public class AuthStateProvider : AuthenticationStateProvider { private readonly HttpClient _httpClient; private readonly ILocalStorageService _localStorage; public AuthStateProvider(HttpClient httpClient, ILocalStorageService localStorage) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _localStorage = localStorage ?? throw new ArgumentNullException(nameof(localStorage)); } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var token = await _localStorage.GetItemAsync<string>(ConstantKeys.LocalToken); if (token == null) { return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); return new AuthenticationState( new ClaimsPrincipal( new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType") ) ); } } }
در اینجا نیز مقدار دهی خودکار httpClient.DefaultRequestHeaders.Authorization را مشاهده میکنید که مقدار token خودش را از Local Storage دریافت میکند که کلید متناظر با آنرا در پروژهی BlazorServer.Common به صورت زیر تعریف کردهایم:
namespace BlazorServer.Common { public static class ConstantKeys { // ... public const string LocalToken = "JWT Token"; } }
- همچنین در اینجا به کمک متد JwtParser.ParseClaimsFromJwt که در ابتدای بحث تهیه کردیم، لیست Claims دریافتی از JWT ارسالی از سمت سرور را تبدیل به یک AuthenticationState قابل استفادهی در برنامهی Blazor WASM کردهایم.
پس از تعریف یک AuthenticationStateProvider سفارشی، باید آنرا به همراه Authorization، به سیستم تزریق وابستگیهای برنامه در فایل Program.cs اضافه کرد:
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddAuthorizationCore(); builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>(); // ... } } }
@using Microsoft.AspNetCore.Components.Authorization
تهیهی سرویسی برای کار با AccountController
اکنون میخواهیم در برنامهی سمت کلاینت، از AccountController سمت سرور که آنرا در قسمت 25 این سری تهیه کردیم، استفاده کنیم. بنابراین نیاز است سرویس زیر را تدارک دید که امکان لاگین، ثبت نام و خروج از سیستم را در سمت کلاینت میسر میکند:
namespace BlazorWasm.Client.Services { public interface IClientAuthenticationService { Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication); Task LogoutAsync(); Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration); } }
namespace BlazorWasm.Client.Services { public class ClientAuthenticationService : IClientAuthenticationService { private readonly HttpClient _client; private readonly ILocalStorageService _localStorage; public ClientAuthenticationService(HttpClient client, ILocalStorageService localStorage) { _client = client; _localStorage = localStorage; } public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication) { var response = await _client.PostAsJsonAsync("api/account/signin", userFromAuthentication); var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize<AuthenticationResponseDTO>(responseContent); if (response.IsSuccessStatusCode) { await _localStorage.SetItemAsync(ConstantKeys.LocalToken, result.Token); await _localStorage.SetItemAsync(ConstantKeys.LocalUserDetails, result.UserDTO); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token); return new AuthenticationResponseDTO { IsAuthSuccessful = true }; } else { return result; } } public async Task LogoutAsync() { await _localStorage.RemoveItemAsync(ConstantKeys.LocalToken); await _localStorage.RemoveItemAsync(ConstantKeys.LocalUserDetails); _client.DefaultRequestHeaders.Authorization = null; } public async Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration) { var response = await _client.PostAsJsonAsync("api/account/signup", userForRegisteration); var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize<RegisterationResponseDTO>(responseContent); if (response.IsSuccessStatusCode) { return new RegisterationResponseDTO { IsRegisterationSuccessful = true }; } else { return result; } } } }
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IClientAuthenticationService, ClientAuthenticationService>(); // ... } } }
- متد LoginAsync، مشخصات لاگین کاربر را به سمت اکشن متد api/account/signin ارسال کرده و در صورت موفقیت این عملیات، اصل توکن دریافتی را به همراه مشخصاتی از کاربر، در Local Storage ذخیره سازی میکند. این مورد سبب خواهد شد تا بتوان به مشخصات کاربر در صفحات دیگر و سرویسهای دیگری مانند AuthStateProvider ای که تهیه کردیم، دسترسی پیدا کنیم. به علاوه مزیت دیگر کار با Local Storage، مواجه شدن با حالتهایی مانند Refresh کامل صفحه و برنامه، توسط کاربر است. در یک چنین حالتی، برنامه از نو بارگذاری مجدد میشود و به این ترتیب میتوان به مشخصات کاربر لاگین کرده، به سادگی دسترسی یافت و مجددا قسمتهای مختلف برنامه را به او نشان داد. نمونهی دیگر این سناریو، بازگشت از درگاه پرداخت بانکی است. در این حالت نیز از یک سرویس سمت سرور دیگر، کاربر به سمت برنامهی کلاینت، Redirect کامل خواهد شد که در اصل اتفاقی که رخ میدهد، با Refresh کامل صفحه یکی است. در این حالت نیز باید بتوان کاربری را که از درگاه بانکی ثالث، به سمت برنامهی کلاینت از نو بارگذاری شده، هدایت شده، بلافاصله تشخیص داد.
- اگر برنامه، Refresh کامل نشود، نیازی به Local Storage نخواهد بود؛ از این لحاظ که در برنامههای سمت کلاینت Blazor، طول عمر تمام سرویسها، صرفنظر از نوع طول عمری که برای آنها مشخص میکنیم، همواره Singleton هستند (ماخذ).
Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
- در متد LoginAsync، علاوه بر ثبت اطلاعات کاربر در Local Storage، مقدار دهی client.DefaultRequestHeaders.Authorization را نیز ملاحظه میکنید. همانطور که عنوان شد، سرویسهای Blazor WASM در اصل دارای طول عمر Singleton هستند. بنابراین تنظیم این هدر در اینجا، بر روی تمام سرویسهای HttpClient تزریق شدهی به سایر سرویسهای برنامه نیز بلافاصله تاثیرگذار خواهد بود.
- متد LogoutAsync، اطلاعاتی را که در حین لاگین موفق در Local Storage ذخیره کردیم، حذف کرده و همچنین client.DefaultRequestHeaders.Authorization را نیز نال میکند تا دیگر اطلاعات لاگین شخص قابل بازیابی نبوده و مورد استفاده قرار نگیرد. همین مقدار برای شکست پردازش درخواستهای ارسالی به منابع محافظت شدهی سمت سرور کفایت میکند.
- متد RegisterUserAsync، مشخصات کاربر در حال ثبت نام را به سمت اکشن متد api/account/signup ارسال میکند که سبب افزوده شدن کاربر جدیدی به بانک اطلاعاتی برنامه و سیستم ASP.NET Core Identity خواهد شد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-31.zip
.
تولید یک پرووایدر منابع دیتابیسی - بخش سوم
برای پیادهسازی ویژگی بهروزرسانی ورودیهای منابع در زمان اجرا راهحلهای مخنلفی ممکن است به ذهن برنامهنویس خطور کند که هر کدام معایب و مزایای خودش را دارد. اما درنهایت بسته به شرایط موجود انتخاب روش مناسب برعهده خود برنامهنویس است.
مثلا برای پرووایدر سفارشی دیتابیسی تهیهشده در مطالب قبلی، تنها کافی است ابزاری تهیه شود تا به کاربران اجازه بهروزرسانی مقادیر موردنظرشان در دیتابیس را بدهد که کاری بسیار ساده است. بدین ترتیب بهروزرسانی این مقادیر در زمان اجرا کاری بسیار ابتدایی به نظر میرسد. اما در قسمت قبل نشان داده شد که برای بالا بردن بازدهی بهتر است که مقادیر موجود در دیتابیس در حافظه سرور کش شوند. استراتژی اولیه و سادهای نیز برای نحوه پیادهسازی این فرایند کشینگ ارائه شد. بنابراین باید امکاناتی فراهم شود تا درصورت تغییر مقادیر کششده در سمت دیتابیس، برنامه از این تغییرات آگاه شده و نسبت به بهروزرسانی این مقادیر در متغیر کشینگ اقدامات لازم را انجام دهد.
اما همانطور که در قسمت قبل نیز اشاره شد، نکتهای که باید درنظر داشت این است که مدیریت تمامی نمونههای تولیدشده از کلاسهای موردبحث کاملا برعهده ASP.NET است، بنابراین دسترسی مستقیمی به این نمونهها در بیرون و در زمان اجرا وجود ندارد تا این ویژگی را بتوان در مورد آنها پیاده کرد.
یکی از روشهای موجود برای حل این مشکل این است که مکانیزمی پیاده شود تا بتوان به تمامی نمونههای تولیدی از کلاس DbResourceManager در بیرون از محیط سیستم مدیریت منابع ASP.NET دسترسی داشت. مثلا یک کلاس حاول متغیری استاتیک جهت ذخیره نمونههای تولیدی از کلاس DbResourceManager، به کتابخانه خود اضافه کرد تا با استفاده از یکسری امکانات بتوان این نمونههای تولیدی را از تغییرات رخداده در سمت دیتابیس آگاه کرد. در این قسمت پیادهسازی این راهحل شرح داده میشود.
.
نکته: قبل از هرچیز برای مناسب شدن طراحی کتابخانه تولیدی و افزایش امنیت آن بهتر است تا سطح دسترسی تمامی کلاسهای پیادهسازی شده تا این مرحله به internal تغییر کند. ازآنجاکه سیستم مدیریت منابع ASP.NET از ریفلکشن برای تولید نمونههای موردنیاز خود استفاده میکند، بنابراین این تغییر تاثیری بر روند کاری آن نخواهد گذاشت.
.
نکته: با توجه به شرایط خاص موجود، ممکن است نامهای استفاده شده برای کلاسهای این کتابخانه کمی گیجکننده باشد. پس با دقت بیشتری به مطلب توجه کنید.
.
پیادهسازی امکان پاکسازی مقادیر کششده
برای اینکار باید تغییراتی در کلاس DbResourceManager داده شود تا بتوان این کلاس را از تغییرات بوجود آمده آگاه ساخت. روشی که من برای این کار درنظر گرفتم استفاده از یک اینترفیس حاوی اعضای موردنیاز برای پیادهسازی این امکان است تا مدیریت این ویژگی در ادامه راحتتر شود.
.
اینترفیس IDbCachedResourceManager
این اینترفیس به صورت زیر تعریف شده است:
namespace DbResourceProvider { internal interface IDbCachedResourceManager { string ResourceName { get; } void ClearAll(); void Clear(string culture); void Clear(string culture, string resourceKey); } }
در پراپرتی فقط خواندنی ResourceName نام منبع کش شده ذخیره خواهد شد.
متد ClearAll برای پاکسازی تمامی ورودیهای کششده استفاده میشود.
متدهای Clear برای پاکسازی ورودیهای کششده یک کالچر به خصوص و یا یک ورودی خاص استفاده میشود.
با استفاده از این اینترفیس، پیادهسازی کلاس DbResourceManager به صورت زیر تغییر میکند:
using System.Collections.Generic; using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { internal class DbResourceManager : IDbCachedResourceManager { 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) { ... } private object GetCachedObject(string resourceKey, string cultureName) { ... } #region Implementation of IDbCachedResourceManager public string ResourceName { get { return _resourceName; } } public void ClearAll() { lock (this) { _resourceCacheByCulture.Clear(); } } public void Clear(string culture) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(culture)) return; _resourceCacheByCulture[culture].Clear(); } } public void Clear(string culture, string resourceKey) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(culture)) return; _resourceCacheByCulture[culture].Remove(resourceKey); } } #endregion } }
اعضای اینترفیس IDbCachedResourceManager به صورت مناسبی در کد بالا پیادهسازی شدند. در تمام این پیادهسازیها مقادیر مربوطه از درون متغیر کشینگ پاک میشوند تا پس از اولین درخواست، بلافاصله از دیتابیس خوانده شوند. برای جلوگیری از دسترسی همزمان نیز از بلاک lock استفاده شده است.
برای استفاده از این امکانات جدید همانطور که در بالا نیز اشاره شد باید بتوان نمونههای تولیدی از کلاس DbResourceManager توسط ASP.NET درون متغیری استاتیک ذخیره شوند. برای اینکار از کلاس جدیدی با عنوان DbResourceCacheManager استفاده میشود که برخلاف تمام کلاسهای تعریفشده تا اینجا با سطح دسترسی public تعریف میشود.
کلاس DbResourceCacheManager
مدیریت نمونههای تولیدی از کلاس DbResourceManager در این کلاس انجام میشود. این کلاس پیادهسازی سادهای بهصورت زیر دارد:
using System.Collections.Generic; using System.Linq; namespace DbResourceProvider { public static class DbResourceCacheManager { internal static List<IDbCachedResourceManager> ResourceManagers { get; private set; } static DbResourceCacheManager() { ResourceManagers = new List<IDbCachedResourceManager>(); } public static void ClearAll() { ResourceManagers.ForEach(r => r.ClearAll()); } public static void Clear(string resourceName) { GetResouceManagers(resourceName).ForEach(r => r.ClearAll()); } public static void Clear(string resourceName, string culture) { GetResouceManagers(resourceName).ForEach(r => r.Clear(culture)); } public static void Clear(string resourceName, string culture, string resourceKey) { GetResouceManagers(resourceName).ForEach(r => r.Clear(culture, resourceKey)); } private static List<IDbCachedResourceManager> GetResouceManagers(string resourceName) { return ResourceManagers.Where(r => r.ResourceName.ToLower() == resourceName.ToLower()).ToList(); } } }
ازآنجاکه نیازی به تولید نمونه ای از این کلاس وجود ندارد، این کلاس به صورت استاتیک تعریف شده است. بنابراین تمام اعضای درون آن نیز استاتیک هستند.
از پراپرتی ResourceManagers برای نگهداری لیستی از نمونههای تولیدی از کلاس DbResourceManager استفاده میشود. این پراپرتی از نوع <List<IDbCachedResourceManager تعریف شده است و برای جلوگیری از دسترسی بیرونی، سطح دسترسی آن internal درنظر گرفته شده است.
در کانستراکتور استاتیک این کلاس (اطلاعات بیشتر درباره static constructor در اینجا) این پراپرتی با مقداردهی به یک نمونه تازه از لیست، اصطلاحا initialize میشود.
سایر متدها نیز برای فراخوانی متدهای موجود در اینترفیس IDbCachedResourceManager پیادهسازی شدهاند. تمامی این متدها دارای سطح دسترسی public هستند. همانطور که میبینید از خاصیت ResourceName برای مشخصکردن نمونه موردنظر استفاده شده است که دلیل آن در قسمت قبل شرح داده شده است.
دقت کنید که برای اطمینان از انتخاب درست همه موارد موجود در شرط انتخاب نمونه موردنظر در متد GetResouceManagers از متد ToLower برای هر دو سمت شرط استفاده شده است.
.
نکته مهم: درباره علت برگشت یک لیست از متد انتخاب نمونه موردنظر از کلاس DbResourceManager در کد بالا (یعنی متد GetResouceManagers) باید نکتهای اشاره شود. در قسمت قبل عنوان شد که سیستم مدیریت منابع ASP.NET نمونههای تولیدی از پرووایدرهای منابع را به ازای هر منبع کش میکند. اما یک نکته بسیار مهم که باید به آن توجه کرد این است که این کش برای «عبارات بومیسازی ضمنی» و نیز «متد مربوط به منابع محلی» موجود در کلاس HttpContext و یا نمونه مشابه آن در کلاس TemplateControl (همان متد GetLocalResourceObject که درباره این متدها در قسمت سوم این سری شرح داده شده است) از یکدیگر جدا هستند و استفاده از هریک از این دو روش موجب تولید یک نمونه مجزا از پرووایدر مربوطه میشود که متاسفانه کنترل آن از دست برنامه نویس خارج است. دقت کنید که این اتفاق برای منابع کلی رخ نمیدهد.
بنابراین برای پاک کردن مناسب ورودیهای کششده در کلاس فوق به جای استفاده از متد Single در انتخاب نمونه موردنظر از کلاس DbResourceManager (در متد GetResouceManagers) از متد Where استفاده شده و یک لیست برگشت داده میشود. چون با توجه به توضیح بالا امکان وجود دو نمونه DbResourceManager از یک منبع درخواستی محلی در لیست نمونههای نگهداری شده در این کلاس وجود دارد.
.
افزودن نمونهها به کلاس DbResourceCacheManager
برای نگهداری نمونههای تولید شده از DbResourceManager، باید در یک قسمت مناسب این نمونهها را به لیست مربوطه در کلاس DbResourceCacheManager اضافه کرد. بهترین مکان برای انجام این عمل در کلاس پایه BaseDbResourceProvider است که درخواست تولید نمونه را در متد EnsureResourceManager درصورت نال بودن آن میدهد. بنابراین این متد را به صورت زیر تغییر میدهیم:
private void EnsureResourceManager() { if (_resourceManager != null) return; { _resourceManager = CreateResourceManager(); DbResourceCacheManager.ResourceManagers.Add(_resourceManager); } }
تا اینجا کار پیادهسازی امکان مدیریت مقادیر کششده در کتابخانه تولیدی به پایان رسیده است.
.
استفاده از کلاس DbResourceCacheManager
پس از پیادهسازی تمامی موارد لازم، حالتی را درنظر بگیرید که مقادیر ورودیهای تعریف شده در منبع "dir1/page1.aspx" تغییر کرده است. بنابراین برای بروزرسانی مقادیر کششده کافی است تا از کدی مثل کد زیر استفاده شود:
DbResourceCacheManager.Clear("dir1/page1.aspx");
کد بالا کل ورودیهای کششده برای منبع "dir1/page1.aspx" را پاک میکند. برای پاک کردن کالچر یا یک ورودی خاص نیز میتوان از کدهایی مشابه زیر استفاده کرد:
DbResourceCacheManager.Clear("Default.aspx", "en-US"); DbResourceCacheManager.Clear("GlobalTexts", "en-US", "Yes");
.
دریافت کد پروژه
کد کامل پروژه DbResourceProvider به همراه مثال و اسکریپتهای دیتابیسی مربوطه از لینک زیر قابل دریافت است:
برای استفاده از این مثال ابتدا باید کتابخانه Entity Framework (با نام EntityFramework.dll) را مثلا از طریق نوگت دریافت کنید. نسخهای که من در این مثال استفاده کردم نسخه 4.4 با حجم حدود 1 مگابایت است.
نکته: در این کد یک بهبود جزئی اما مهم در کلاس ResourceData اعمال شده است. در قسمت سوم این سری، اشاره شد که نام ورودیهای منابع Case Sensitive نیست. بنابراین برای پیادهسازی این ویژگی، متدهای این کلاس باید به صورت زیر تغییر کنند:
public Resource GetResource(string resourceKey, string culture) { using (var data = new TestContext()) { return data.Resources.SingleOrDefault(r => r.Name.ToLower() == _resourceName.ToLower() && r.Key.ToLower() == resourceKey.ToLower() && r.Culture == culture); } } public List<Resource> GetResources(string culture) { using (var data = new TestContext()) { return data.Resources.Where(r => r.Name.ToLower() == _resourceName.ToLower() && r.Culture == culture).ToList(); } }
.
در آینده...
در ادامه مطالب، بحث تهیه پرووایدر سفارشی فایلهای resx. برای پیادهسازی امکان بهروزرسانی در زمان اجرا ارائه خواهد شد. بعد از پایان تهیه این پرووایدر سفارشی، این سری مطالب با ارائه نکات استفاده از این پرووایدرها در ASP.NET MVC پایان خواهد یافت.
.
منابع