Popover بوت استرپ برای کار با منابع remote طراحی نشدهاست و نیاز است توابع API آنرا به همراه jQuery Ajax ترکیب کرد تا به تصویر فوق رسید.
مرحلهی اول: اکشن متدی که یک partial view را باز میگرداند
فرض کنید اکشن متدی که لیست کاربران رای دادهی به یک مطلب را باز میگرداند، چنین شکلی را دارد:
public ActionResult RenderResults(string param1) { var users = new[] { new User{ Id = 1, Name = "Test 1", Rating = 3}, new User{ Id = 2, Name = "Test 2", Rating = 4}, new User{ Id = 3, Name = "Test 3", Rating = 5} }; return PartialView("_RenderResults", model: users); }
@using RemotePopOver.Models @model IList<User> <ul id="ratings1" data-title="Ratings" class="list-unstyled"> @foreach (var user in Model) { <li> @user.Name <span class="badge pull-right">@user.Rating</span> </li> } </ul>
مرحلهی دوم: دریافت اطلاعات partial view با استفاده از jQuery Ajax و سپس درج آن در یک popover
میخواهیم با حرکت ماوس بر روی دکمهی سفارشی ذیل، یک popover ظاهر شده و محتوای خودش را از اکشن متد فوق تامین کند.
<span id="remotePopover1" aria-hidden="true" data-param1="test" data-popover-content-url="@Url.Action("RenderResults", "Home")" class="glyphicon glyphicon-info-sign btn btn-info"></span>
در ادامه نحوهی استفادهی از این ویژگیها را در jQuery Ajax مشاهده میکنید:
@section Scripts { <script type="text/javascript"> $(document).ready(function () { $('body').on('mouseenter', 'span[data-popover-content-url]', function () { var el = $(this); $.ajax({ type: "POST", url: $(this).data("popover-content-url"), data: JSON.stringify({ param1: $(this).data("param1") }), contentType: "application/json; charset=utf-8", dataType: "json", // controller is returning a simple text, not json complete: function (xhr, status) { var data = xhr.responseText; if (status === 'error' || !data) { el.popover({ content: 'Error connecting server!', trigger: 'focus', html: true, container: 'body', placement: 'auto', title: 'Error!' }).popover('show'); } else { el.popover({ content: data, trigger: 'focus', html: true, container: 'body', placement: 'auto', title: $('<html />').html(data).find('#ratings1:first').data('title') }).popover('show'); } } }); }).on('mouseleave', 'span[data-popover-content-url]', function () { $(this).popover('hide'); }); }); </script> }
خروجی partial view به صورت json نیست. بنابراین باید اطلاعات نهایی آنرا در callback ویژهی complete دریافت کرد. مقدار data دریافتی، معادل اطلاعات رندر شدهی partial view است. به همین جهت آنرا به خاصیت content متد popver ارسال میکنیم. همچنین چون خروجی patrtial view به همراه html است، نیاز است خاصیت html متد popover نیز به true تنظیم شود. در خاصیت title، نحوهی دسترسی به مقدار data-title تنظیم شدهی در partial view را مشاهده میکنید.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
RemotePopOver.zip
در مورد معرفی مقدماتی MEF میتوانید به این مطلب مراجعه کنید و در مورد الگوی Singleton به اینجا.
کاربردهای الگوی Singleton عموما به شرح زیر هستند:
1) فراهم آوردن دسترسی ساده و عمومی به DAL (لایه دسترسی به دادهها)
2) دسترسی عمومی به امکانات ثبت وقایع سیستم در برنامه logging -
3) دسترسی عمومی به تنظیمات برنامه
و موارد مشابهی از این دست به صورتیکه تنها یک روش دسترسی به این اطلاعات وجود داشته باشد و تنها یک وهله از این شیء در حافظه قرار گیرد.
با استفاده از امکانات MEF دیگر نیازی به نوشتن کدهای ویژه تولید کلاسهای Singleton نمیباشد زیرا این چارچوب کاری دو نوع روش وهله سازی از اشیاء (PartCreationPolicy) را پشتیبانی میکند: Shared و NonShared . حالت Shared دقیقا همان نام دیگر الگوی Singleton است. البته لازم به ذکر است که حالت Shared ، حالت پیش فرض تولید وهلهها بوده و نیازی به ذکر صریح آن همانند ویژگی زیر نیست:
[PartCreationPolicy(CreationPolicy.Shared)]
مثال:
فرض کنید قرار است از کلاس زیر تنها یک وهله بین صفحات یک برنامهی Silverlight توزیع شود. با استفاده از ویژگی Export به MEF اعلام کردهایم که قرار است سرویسی را ارائه دهیم :
using System;
using System.ComponentModel.Composition;
namespace SlMefTest
{
[Export]
public class WebServiceData
{
public int Result { set; get; }
public WebServiceData()
{
var rnd = new Random();
Result = rnd.Next();
}
}
}
کدهای صفحه اصلی برنامه (که از یک دکمه و یک Stack panel جهت نمایش محتوای یوزر کنترل تشکیل شده) به شرح بعد هستند:
<UserControl x:Class="SlMefTest.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
<StackPanel>
<Button Content="MainPageButton" Height="23"
HorizontalAlignment="Left"
Margin="10,10,0,0" Name="button1"
VerticalAlignment="Top" Width="98" Click="button1_Click" />
<StackPanel Name="panel1" Margin="5"/>
</StackPanel>
</UserControl>
using System.ComponentModel.Composition;
using System.Windows;
namespace SlMefTest
{
public partial class MainPage
{
[Import]
public WebServiceData Data { set; get; }
public MainPage()
{
InitializeComponent();
this.Loaded += mainPageLoaded;
}
void mainPageLoaded(object sender, RoutedEventArgs e)
{
CompositionInitializer.SatisfyImports(this);
panel1.Children.Add(new SilverlightControl1());
}
private void button1_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(Data.Result.ToString());
}
}
}
کدهای User control ساده اضافه شده به شرح زیر هستند:
<UserControl x:Class="SlMefTest.SilverlightControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<Button Content="UserControlButton"
Height="23"
HorizontalAlignment="Left"
Margin="10,10,0,0"
Name="button1"
VerticalAlignment="Top"
Width="125"
Click="button1_Click" />
</Grid>
</UserControl>
using System.ComponentModel.Composition;
using System.Windows;
namespace SlMefTest
{
public partial class SilverlightControl1
{
[Import]
public WebServiceData Data { set; get; }
public SilverlightControl1()
{
InitializeComponent();
this.Loaded += silverlightControl1Loaded;
}
void silverlightControl1Loaded(object sender, RoutedEventArgs e)
{
CompositionInitializer.SatisfyImports(this);
}
private void button1_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show(Data.Result.ToString());
}
}
}
ممنوع کردن استفادهی از ساختارهای دادهی غیرجنریک
قدم اول مواجه شدن با یک چنین کدهای قدیمی، ممنوع کردن استفادهی از ساختارهای دادهی غیرجنریک و الزام به تبدیل آنها به نوعهای جدید است. برای این منظور میتوان از Microsoft.CodeAnalysis.BannedApiAnalyzers استفاده کرد که توضیحات بیشتر آنرا در مطلب «غنی سازی کامپایلر C# 9.0 با افزونهها» پیشتر بررسی کردهایم. به صورت خلاصه، ابتدا بستهی نیوگت آنرا به صورت یک آنالایزر جدید به فایل csproj. برنامه معرفی میکنیم:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> <ItemGroup> <AdditionalFiles Include="$(MSBuildThisFileDirectory)BannedSymbols.txt" Link="Properties/BannedSymbols.txt"/> </ItemGroup> </Project>
# https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.BannedApiAnalyzers/BannedApiAnalyzers.Help.md T:System.Collections.ICollection;Don't use a non-generic data structure. T:System.Collections.Hashtable;Don't use a non-generic data structure. T:System.Collections.ArrayList;Don't use a non-generic data structure. T:System.Collections.SortedList;Don't use a non-generic data structure. T:System.Collections.Stack;Don't use a non-generic data structure. T:System.Collections.Queue;Don't use a non-generic data structure.
پیشنهاد یک دیکشنری کم دردسرتر!
برای نمونه پس از تنظیمات فوق، مجبور به تغییر تمام hash tableها به دیکشنریهای جدید جنریک خواهیم شد؛ اما ... اگر اینکار را انجام دهیم، برنامهای که تا پیش از این بدون مشکل کار میکرد، اکنون با استثناهای متعدد یافت نشدن کلیدها، خاتمه پیدا میکند! چون دیگر دیکشنریهای جدید، همانند hash tableهای قدیمی، در صورت عدم وجود کلیدی، نال را بازگشت نمیدهند.
برای رفع این مشکل و اصلاح انبوهی از کدها با حداقل تغییرات و عدم تکرار TryGetValueها در همهجا، میتوان دسترسی به ایندکسهای یک دیکشنری استاندارد دات نت را به صورت زیر با ارثبری از آن، بازنویسی کرد:
/// <summary> /// This custom IDictionary doesn't throw a KeyNotFoundException while accessing its value by a given key /// </summary> public interface INullValueDictionary<TKey, TValue> : IDictionary<TKey, TValue> { new TValue this[TKey key] { get; set; } } /// <summary> /// This custom IDictionary doesn't throw a KeyNotFoundException while accessing its value by a given key /// </summary> public class NullValueDictionary<TKey, TValue> : Dictionary<TKey, TValue>, INullValueDictionary<TKey, TValue> { public new TValue this[TKey key] { get => TryGetValue(key, out var val) ? val : default; set => base[key] = value; } }
واژه XSS مخفف Cross-site scripting، نوعی از آسیب پذیریست که در برنامههای تحت وب نمود پیدا میکند. به طور کلی و خلاصه، این آسیب پذیری به فرد نفوذ کننده اجازه تزریق اسکریپتهایی را به صفحات وب، میدهد که در سمت کاربر اجرا میشوند ( Client Side scripts ) . در نهایت این اسکریپتها توسط سایر افرادی که از صفحات مورد هدف قرار گرفته بازدید میکنند اجرا خواهد شد.
هدف از این نوع حمله :
بدست آوردن اطلاعات کوکیها و سشنهای کاربران ( مرتبط با آدرسی که صفحه آلوده شده در آن قرار دارد ) است. سپس فرد نفوذ کننده متناسب با اطلاعات بدست آمده میتواند به اکانت شخصی کاربران مورد هدف قرار گرفته، نفوذ کرده و از اطلاعات شخصی آنها سوء استفاده کند .
به صورت کلی دو طبقه بندی برای انواع حملات Cross-site scripting وجود دارند.
حملات XSS ذخیره سازی شده ( Stored XSS Attacks ) :
در این نوع ، کدهای مخرب تزریق شده، در سرور سایت قربانی ذخیره میشوند. محل ذخیره سازی میتواند دیتابیس سایت یا هر جای دیگری که دادهها توسط سایت یا برنامه تحت وب بازیابی میشوند و نمایش داده میشوند باشد. اما اینکه چگونه کدهای مخرب در منابع یاد شده ذخیره میشوند؟
فرض کنید در سایت جاری آسیب پذیری مذکور وجود دارد. راههای ارسال دادهها به این سایت چیست؟ نویسندگان میتوانند مطلب ارسال کنند و کاربران میتوانند نظر دهند. حال اگر در یکی از این دو بخش بررسیهای لازم جهت مقابله با این آسیب پذیری وجود نداشته باشد و نوشتههای کاربران که میتواند شامل کدهای مخرب باشد مستقیما در دیتابیس ذخیره شده و بدون هیچ اعتبار سنجی نمایش داده شود چه اتفاقی رخ خواهد داد؟ مسلما با بازدید صفحه آلوده شده، کدهای مخرب بر روی مرورگر شما اجرا و کوکیهای سایت جاری که متعلق به شما هستند برای هکر ارسال میشود و ...
حملات XSS منعکس شده ( Reflected XSS Attacks ) :
در این نوع از حمله، هیچ نوع کد مخربی در منابع ذخیره سازی وبسایت یا اپلیکیشن تحت وب توسط فرد مهاجم ذخیره نمیشود ! بلکه از ضعف امنیتی بخشهایی همچون بخش جستجو وب سایت، بخشهای نمایش پیغام خطا و ... استفاده میشود ... اما به چه صورت؟
در بسیاری از سایتها، انجمنها و سیستمهای سازمانی تحت وب، مشاهده میشود که مثلا در بخش جستجو، یک فیلد برای وارد کردن عبارت جستجو وجود دارد. پس از وارد کردن عبارت جستجو و submit فرم، علاوه بر نمایش نتایج جستجو، عبارت جستجو شده نیز به نمایش گذاشته میشود و بعضا در بسیاری از سیستمها این عبارت قبل از نمایش اعتبار سنجی نمیشود که آیا شامل کدهای مخرب میباشد یا خیر. همین امر سبب میشود تا اگر عبارت جستجو شامل کدهای مخرب باشد، آنها به همراه نتیجهی جستجو اجرا شوند.
اما این موضوع چگونه مورد سوء استفاده قرار خواهد گرفت؟ مگر نه اینکه این عبارت ذخیره نمیشود پس با توضیحات فوق، کد فقط بر روی سیستم مهاجم که کد جستجو را ایجاد میکند اجرا میشود، درست است؟ بله درست است ولی به نقطه ضعف زیر توجه کنید ؟
www.test.com/search?q=PHNjcmlwdD5hbGVydChkb2N1bWVudC5jb29raWUpOzwvc2NyaXB0Pg==
این آدرس حاصل submit شدن فرم جستجو وبسایت test (نام وبسایت واقعی نیست و برای مثال است ) و ارجاع به صفحه نتایج جستجو میباشد. در واقع این لینک برای جستجوی یک کلمه یا عبارت توسط این وبسایت تولید شده و از هر کجا به این لینک مراجعه کنید عبارت مورد نظر مورد جستجو واقع خواهد شد. در واقع عبارت جستجو به صورت Base64 به عنوان یک query String به وبسایت ارسال میشود؛ علاوه بر نمایش نتایج، عبارت جستجو شده نیز به کاربر نشان داده شده و اگر آسیب پذیری مورد بحث وجود داشته باشد و عبارت شامل کدهای مخرب باشد، کدهای مخرب بر روی مرورگر فردی که این لینک را باز کرده اجرا خواهد شد!
در این صورت کافیست فرد مهاجم لینک مخرب را به هر شکلی به فرد مورد هدف بدهد ( مثلا ایمیل و ... ). حال در صورتیکه فرد لینک را باز کند (با توجه به اینکه لینک مربوط به یک سایت معروف است و عدم آگاهی کاربر از آسیب پذیری موجود در لینک، باعث باز کردن لینک توسط کاربر میشود)، کدها بر روی مرورگرش اجرا شده و کوکیهای سایت مذکور برای مهاجم ارسال خواهد شد ... به این نوع حمله XSS ، نوع انعکاسی میگویند که کاملا از توضیحات فوق الذکر، دلیل این نامگذاری مشخص میباشد.
اهمیت مقابله با این حمله :
برای نمونه این نوع باگ حتی تا سال گذشته در سرویس ایمیل یاهو وجود داشت. به شکلی که یکی از افراد انجمن hackforums به صورت Private این باگ را به عنوان Yahoo 0-Day XSS Exploit در محیط زیر زمینی و بازار سیاه هکرها به مبلغ چند صد هزار دلار به فروش میرساند. کاربران مورد هدف کافی بود تا فقط یک ایمیل دریافتی از هکر را باز کنند تا کوکیهای سایت یاهو برای هکر ارسال شده و دسترسی ایمیلهای فرد قربانی برای هکر فراهم شود ... ( در حال حاظر این باگ در یاهو وجو ندارد ).
چگونگی جلوگیری از این آسیب پذیری
در این سری از مقالات کدهای پیرامون سرفصلها و مثالها با ASP.net تحت فریم ورک MVC و به زبان C# خواهند بود. هر چند کلیات مقابله با آسیب پذیری هایی از این دست در تمامی زبانها و تکنولوژیهای تحت وب یکسان میباشند.
خوشبختانه کتابخانهای قدرتمند برای مقابله با حمله مورد بحث وجود دارد با نام AntiXSS که میتوانید آخرین نسخه آن را با فرمان زیر از طریق nugget به پروژه خود اضافه کنید. البته ذکر این نکته حائز اهمیت است که Asp.net و فریم ورک MVC به صورت توکار تا حدودی از بروز این حملات جلوگیری میکند. برای مثال به این صورت که در View ها شما تا زمانی که از MvcHtmlString استفاده نکنید تمامی محتوای مورد نظر برای نمایش به صورت Encode شده رندر میشوند. این داستان برای Url ها هم که به صورت پیش فرض encode میشوند صدق میکند. ولی گاها وقتی شما برای ورود اطلاعات مثلا از یک ادیتور WYSWYG استفاده میکنید و نیاز دارید دادهها را بدون encoding رندر کنید. آنگاه به ناچار مجاب بر اعمال یک سری سیاستهای خاصتر بر روی داده مورد نظر برای رندر میشوید و نمیتوانید از encoding توکار فوق الذکر استفاده کنید. آنگاه این کتابخانه در اعمال سیاستهای جلوگیری از بروز این آسیب پذیری میتواند برای شما مفید واقع شود.
PM> Install-Package AntiXSS
… var reviewContent = model.UserReview; reviewContent = Microsoft.Security.Application.Encoder.HtmlEncode(review); …
امیدوارم در اولین بخش از این سری مقالات، به صورت خلاصه مطالب مهمی که باعث ایجاد فهم کلی در رابطه با حملات Xss وجود دارد، برای دوستان روشن شده و پیش زمینه فکری برای مقابله با این دست از حملات برایتان به وجود آمده باشد.
Plan چیست؟
در اینجا Plan کوئری سادهای را مشاهده میکنید. کار آن انتخاب نام، نام خانوادگی و آدرس ایمیل افرادی است که نام خانوادگی آنها با Whit شروع میشود و بر روی دو جدول که با هم جوین شدهاند عمل میکند.
اولین موردی را که باید در یک Plan به آن دقت کرد، عملگرهای آن است که شامل select، nested loop، index seek و clustered index seek میباشند. index seek بر روی جدول اشخاص و clustered index seek بر روی جدول ایمیلها صورت میگیرد. nested loop بیانگر جوین بین جداول است. این عملگرها بیانگر اعمال فیزیکی هستند که رخ دادهاند.
همچنین تعدادی پیکان (arrow) را هم مشاهده میکنید که بیانگر جهت سیلان دادهها است. اطلاعات از طریق index seek و clustered index seek به nested loop میرسند و در نهایت به عملگر select ارائه خواهند شد.
در این تصویر، هزینههای تخمینی مرتبط با هر عملگر نیز قابل مشاهدهاست که نسبت به کل کوئری محاسبه شدهاند. این هزینه، بدون واحد است و به معنای میزان زمان و یا CPU صرف شدهی برای انجام عمل خاصی نیست و صرفا برای مقایسهی هزینهی نسبی عملگرها در کل یک Plan کاربرد دارد. باید دقت داشت که هزینههای نمایش داده شدهی در یک Plan، همیشه تخمینی هستند. در قسمتهای قبل در مورد نحوهی دریافت estimated plan و actual plan بحث کردیم. هیچگاه چیزی به نام Actual cost در یک Actual plan وجود ندارد و همیشه تخمینی است. روش محاسبهی آنها توسط الگوریتمهای بهینه ساز است و مستقل از سخت افزار مورد استفاده.
در یک پلن، مدت زمان انجام یک کوئری، میزان I/O ، locks و wait statistics قابل مشاهده نیستند. البته اگر از SQL Server 2016 به بعد استفاده میکنید و یک Actual plan را محاسبه کردهاید، مدت زمان انجام یک کوئری و میزان I/O نیز در Plan قابل مشاهدهاند.
از چه جهتی باید یک Plan را خواند؟
اگر هدف، بررسی «سیلان کنترل» است (Control flow)، باید یک Plan را از «چپ به راست» خواند. یعنی از عملگر select شروع میکنیم که کوئری ما را کنترل میکند. سپس به nested loop میرسیم که نام و نام خانوادگی را از جدول اشخاص دریافت میکند. این nested loop نیز با کمک ایندکسهای تعریف شده، شرط کوئری را بر آورده میکند.
اما جهت «سیلان اطلاعات» در یک Plan از «راست به چپ» است (Data flow). اطلاعات از طریق index seekها به حلقه و سپس select میرسند.
چگونه یک Query Plan را شروع به بررسی کنیم؟
ابتدا در management studio از منوی Query، گزینهی Include actual execution plan را انتخاب میکنیم. سپس کوئری زیر را اجرا میکنیم:
USE [WideWorldImporters]; GO SELECT [s].[StateProvinceName], [s].[SalesTerritory], [s].[LatestRecordedPopulation], [s].[StateProvinceCode] FROM [Application].[Countries] [c] JOIN [Application].[StateProvinces] [s] ON [s].[CountryID] = [c].[CountryID] WHERE [c].[CountryName] = 'United States'; GO
در اینجا چهار عملگر select، nested loop، clustered index seek و clustered index scan مشاهده میشوند. شاید اینطور به نظر برسد که در این Plan، ابتدا clustered index scan و clustered index seek انجام میشوند و سپس به nested loop میرسیم (اگر Plan را بر اساس سیلان داده، از راست به چپ بخوانیم)؛ اما اینطور نیست. عملگرها در اینجا در حقیقت یک سری iterator هستند که با دریافت ردیفهای مرتبط، بلافاصله آنها را به nested loop ارسال میکنند. این nested loop نیز ردیفهایی را که با جوین انجام شده تطابق دارند، به سمت select ارسال میکند.
اگر به تصویر دقت کنید هر کدام از ایندکسها به یک جدول اشاره میکنند که نام آن بالای عدد هزینه درج شدهاست. برای مشاهده نام کامل شیء متناظر با آن، میتوان اشارهگر ماوس را بر روی ایندکس حرکت داد و به اطلاعات قسمت Object دقت کرد:
و یا اگر اطلاعات کاملتری از این popup را نیاز داشتید، عملگر مدنظر را انتخاب کرده و سپس دکمهی F4 را فشار دهید:
در برگهی خواص ظاهر شده میتوان ریز جزئیات تمام اطلاعات مرتبط با عملگر انتخاب شده را مشاهده کرد. برای مثال در اینجا حتی اطلاعات Logical reads را بدون روشن کردن SET STATISTICS IO ON میتوان مشاهده کرد:
همچنین با توجه به انتخاب گزینهی Include actual execution plan، تعداد ردیفهای بازگشت داده شدهی واقعی و تخمینی، با هدایت اشارهگر ماوس بر روی یکی از اشیاء مرتبط با بررسی ایندکسها، قابل مشاهده هستند:
گزارش این تعداد ردیفها، با حرکت اشارهگر ماوس، بر روی پیکانهای منتهی به nested loop و یا select نیز قابل مشاهده هستند:
به این ترتیب میتوان دریافت که چه مقدار اطلاعات در طول این Plan و قسمتهای مختلف آن، از سمت راست به چپ، در حال جابجایی است.
اکنون در ادامه سعی میکنیم توسط DMO's، این Plan را از Plan cache دریافت کنیم:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT [cp].[size_in_bytes], [cp].[cacheobjtype], [cp].[objtype], [cp].[plan_handle], [dest].[text], [plan].[query_plan] FROM [sys].[dm_exec_cached_plans] [cp] CROSS APPLY [sys].[dm_exec_sql_text]([cp].[plan_handle]) [dest] CROSS APPLY [sys].[dm_exec_query_plan]([cp].[plan_handle]) [plan] WHERE [dest].[text] LIKE '%StateProvinces%' OPTION(MAXDOP 1, RECOMPILE);
همانطور که مشاهده میکنید، اینبار تنها اطلاعات تخمینی در این Plan ظاهر شدهاند؛ چون اطلاعات آن از کش خوانده شدهاست. همچنین در اینجا اطلاعات I/O مانند حالت Actual Plan، در برگهی خواص عملگرهای این Plan، قابل مشاهده نیستند.
نگاهی به اطلاعات XML ای یک Plan
اگر کوئری زیر را با فرض انتخاب Include actual execution plan در منوی Query اجرا کنیم:
SELECT [o].[OrderID], [ol].[OrderLineID], [o].[OrderDate], [o].[CustomerID], [ol].[Quantity], [ol].[UnitPrice] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID]; GO
در اینجا با کلیک راست بر روی Plan، میتوان گزینهی Show Execution Plan XML را نیز انتخاب کرد. گاهی از اوقات کار کردن با این اطلاعات، به صورت XML ای سادهتر است و فرمت آن از هر نگارش به نگارش دیگر SQL Server میتواند متفاوت باشد.
برای مثال اگر در برگهی نمایش این اطلاعات، دکمههای ctrl+f را فشرده و به دنبال runtime بگردیم، خیلی سریعتر میتوان به اطلاعات I/O ،CPU و تعداد ردیفهای بازگشت داده شده، رسید.
و یا حتی اطلاعات wait statistics را نیز میتوان به سادگی در اینجا مشاهده کرد تا مشخص شود چرا یک کوئری خوب عمل نمیکند:
اجرای چند کوئری با هم و بررسی Query Plan آنها
اگر دو کوئری زیر را با فرض انتخاب Include actual execution plan در منوی Query با هم اجرا کنیم:
USE [WideWorldImporters]; GO SELECT [CustomerID], [TransactionAmount] FROM [Sales].[CustomerTransactions] WHERE [CustomerID] = 1056; GO SELECT [o].[OrderID], [ol].[OrderLineID], [o].[OrderDate], [o].[CustomerID], [ol].[Quantity], [ol].[UnitPrice] FROM [Sales].[Orders] [o] JOIN [Sales].[OrderLines] [ol] ON [o].[OrderID] = [ol].[OrderID]; GO
هزینهی اولین کوئری نسبت به کل batch جاری، 10 درصد است و هزینهی دومین کوئری، 90 درصد. بنابراین اگر چندین کوئری را با هم اجرا کنیم، به این صورت میتوان هزینهی هر کدام را نسبت به کل عملیات، تخمین بزنیم. در هر کوئری نیز هزینههایی درج شدهاند که صرفا متعلق به همان کوئری هستند. برای مثال در اولین کوئری، key lookup سنگینترین عملگر کل کوئری است.
افزونهای برای SQL server 2005 به نام Database Publishing Wizard وجود داشت/دارد که توسعهی آن به ظاهر برای SQL server 2008 متوقف شده است. توسط این افزونه میتوان رکوردهای یک دیتابیس را به صورت عبارات T-SQL درآورد (هر رکورد را به صورت خودکار تبدیل به یک دستور insert میکند). به این صورت کار انتقال دیتا خصوصا به هاستهایی که دسترسی مستقیم restore کردن داده را نمیدهند، به سادگی صورت میگیرد. تنها کافی است خروجی کار یکبار بر روی دیتابیس مقصد اجرا شود تا رکوردهایی دقیقا با همان اطلاعات دیتابیس منبع در آن ایجاد گردند.
این قابلیت اکنون جزئی از management studio اس کیوال سرور 2008 است.
برای استفاده از آن بر روی دیتابیس مورد نظر در management studio 2008 کلیک راست کرده و گزینه زیر را انتخاب کنید:
در صفحه ویزاردی که ظاهر میشود، بر روی next کلیک کرده و در صفحهی بعدی گزینه script data را یافته و مقدار آنرا به true تنظیم نمائید (شکل زیر).
سپس بر روی next کلیک کرده و در صفحه بعد گزینه tables را انتخاب کنید. در ادامه با کلیک بر روی دکمه next ، در صفحهی بعدی میتوان تمامی جداول و یا تنها جداول مورد نظر را جهت تهیه اسکریپت دادههای آنها انتخاب نمود و در آخر عملیات تهیه اسکریپت insert دادهها به صورت خودکار صورت خواهد گرفت.
برای مشاهده تصاویر بیشتری از این عملیات ساده میتوان به این مقاله رجوع نمود.
شایان ذکر است این قابلیت با نگارشهای پائینتر اس کیوال سرور نیز کار میکند (برای مثال اتصال به اس کیوال سرور 2000 از طریق management studio 2008).
Protocol Buffers فرمتی برای تبادل دیتا
در واقع ما یک سیستمی داریم که شامل مدلی است از دیتاهای ما و از این مدل برای کوئری گرفتن از دیتابیس استفاده میشود، که البته برای بیشتر پروژههای نرم افزاری، معماری درست و ترجیح داده شدهای هم میباشد.
زمانیکه نیازهای پروژه روز به روز افزورده و پیچیدهتر میشود، مدل CRUD بصورت پیوسته از ارزشش کاسته میشود و از آن سادگی اولیهی در درک و خوانایی آن دور خواهد شد.
ذات CQRS بر آن است که شما مدلهای مختلفی را برای خواندن و نوشتن دیتا داشته باشید. الگوی آن چیزی شبیه به تصویر زیر است
ES از برنامه نویسان میخواهد که مدل سنتی CRUD را فراموش کرده و بجای آن تغییراتی را که روی دیتا صورت گرفته، نیز درج نمایند. اینکار به وسیلهی یک دیتابیس Append-only انجام میشود که به نام Event Store شناخته میشود.
در این معماری ما همهی تغییرات روی دیتا را به صورت Serialize Event ذخیره میکنیم که میتواند دوباره در هر زمانی اجرا شده و current state هر objectی را در اختیار بگذارد.
این روش به ما کمک بزرگی میکند تا وضعیت یک object را در گذشته به راحتی پیدا کنیم و از آن میتوان به غیر از فوایدی که دارد، به عنوان یک Logger نیز استفاده نمود. به دلیل اینکه جزء به جزء تغییرات بر روی state سیستم، در آن ثبت شده است. از آنجاییکه دیتا بصورت serialize ذخیره میشود، بارگزاری آن نیز با سرعت بالایی انجام خواهد شد.
public class Movie : AggregateRoot { public string Title { get; set; } public DateTime ReleaseDate { get; set; } public int RunningTimeMinutes { get; set; } public Movie() { } public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes) { //پیاده سازی خواهد شد } }
public class CreateMovieCommand : ICommand { public string Title { get; set; } public DateTime ReleaseDate { get; set; } public int RunningTimeMinutes { get; set; } public CreateMovieCommand(string title, DateTime releaseDate, int runningTime) { Title = title; ReleaseDate = releaseDate; RunningTimeMinutes = runningTime; } }
public class CreateMovieCommandHandler : CommandHandler<CreateMovieCommand> { protected IDomainRepository _repository; public CreateMovieCommandHandler(IDomainRepository repository) { _repository = repository; } public override void Handle(CreateMovieCommand command) { var movie = new Domain.Movie(Guid.NewGuid(), command.Title, command.ReleaseDate, command.RunningTimeMinutes); _repository.Save(movie); } }
public class MovieCreatedEvent : DomainEvent { public Guid MovieId { get { return AggregateRootId; } set { AggregateRootId = value;} } public string Title { get; set; } public DateTime ReleaseDate { get; set; } public int RunningTimeMinutes { get; set; } public MovieCreatedEvent(Guid movieId, string title, DateTime releaseDate, int runningTime) { MovieId = movieId; Title = title; ReleaseDate = releaseDate; RunningTimeMinutes = runningTime; } }
public class Movie : AggregateRoot { public string Title { get; set; } public DateTime ReleaseDate { get; set; } public int RunningTimeMinutes { get; set; } public Movie(Guid movieId, string title, DateTime releaseDate, int runningTimeMinutes) { Apply(new MovieCreatedEvent(Guid.NewGuid(), title, releaseDate, runningTimeMinutes)); } }
public class MovieEventHandler : IHandleDomainEvents<MovieCreatedEvent> { public void Handle(MovieCreatedEvent createdEvent) { using (MoviesContext entities = new MoviesContext()) { entities.Movies.Add(new Movie() { Id = createdEvent.AggregateRootId, Title = createdEvent.Title, ReleaseDate = createdEvent.ReleaseDate, RunningTimeMinutes = createdEvent.RunningTimeMinutes }); entities.SaveChanges(); } } }
protected void OnMovieCreated(MovieCreatedEvent domainEvent) { Id = domainEvent.AggregateRootId; Title = domainEvent.Title; ReleaseDate = domainEvent.ReleaseDate; RunningTimeMinutes = domainEvent.RunningTimeMinutes; }
- ذاتا پیاده سازی این مدل سخت و دشوار است و از آنجاییکه سادگی در پیاده سازی سیستمهای نرم افزاری، یک اصل مهم محسوب میشود، بنابراین استفاده از این مدل محدود میشود به سیستمهای نرم افزاری که مزیتهای گفته شده در قسمت فوق برایشان حیاتی محسوب شود.
- برای پیاده سازی سیستمی با این مدل احتیاج به تیم توسعهای است که با مفاهیم آن کاملا آشنا باشد.
- هر چند امروزه فضای فیزیکی برای ذخیره سازی دیتا ارزان محسوب میشود، اما به هر حال استفاده از این مدل به همراه ES، حجم زیادی از Disk space را خواهد گرفت.
- همانطور که دیدید برای پیاده سازی یک Insert ساده، حجم زیادی کد نوشته شدهاست. بنابراین تولید اینگونه نرم افزارها به زمان بیشتری نیاز دارد.