ASP.NET MVC #4
یه خواهش:
امکان داره theme و Setting مربوط به Visual Studio خود را برای دانلود قرار بدهید. از هر سایتی setting گرفتم مثل setting شما در عکس بالا نشد.
پیشاپیش ممنون.
فرض کنید میخواهید عددی را در کامپوننتی، دریافت کنید. میتوان اینکار را با تعریف یک پارامتر عمومی به صورت زیر انجام داد:
[Parameter] public int Value { get; set; }
برای حل این نوع مشکلات، میتوان در Blazor پارامترها را جنریک تعریف کرد. برای نمونه اگر به طراحی یک سری کامپوننتهای پایهای Blazor دقت کنیم، امکان دریافت مستقیم انواع و اقسام اعداد و یا انواع و اقسام نوعهای مرتبط با زمان را دارند؛ بدون اینکه این اطلاعات را به صورت رشتهای دریافت کنند و یا به ازای هر نوع عددی، یک پارامتر جداگانه را تعریف و یا اینکه استفاده کننده را محدود به ورود تنها یک نوع عددی و یا زمانی خاص کرده باشند.
نحوهی جنریک تعریف کردن یک کامپوننت در Blazor
Blazor فایلهای razor. را در حین پروسهی build، به cs. تبدیل میکند و از آنجائیکه فایلهای razor. الزامی به تعریف نام کلاس مرتبط را ندارند و این نام به صورت خودکار دقیقا از نام خود فایل، دریافت میشود، نیاز است راهی را یافت تا بتوان کلاس مرتبط را به صورت Generic تعریف کرد. برای این منظور، دایرکتیو ویژهای به نام typeparam@ پیش بینی شدهاست که توسط آن میتوان یک یا چند نوع/پارامتر جنریک جدا شدهی توسط کاما را تعریف کنیم (تعریف شدهی در فایل فرضی MyGenericComponent.razor):
@typeparam T @code { [Parameter] public T Value { get; set; } }
نحوهی جنریک تعریف کردن کامپوننتهای دارای code-behind
اگر قسمت code@ فایلهای razor. را به فایلهای code-benind منتقل میکنید و داخل فایل razor.، کد #C نمینویسید، روش جنریک تعریف کردن یک کامپوننت، به صورت زیر خواهد بود (برای مثال دو فایل MyGenericComponentWithCodeBehind.razor و MyGenericComponentWithCodeBehind.razor.cs تعریف شدهاند):
using Microsoft.AspNetCore.Components; namespace BlazorGenericComponents.Pages; public partial class MyGenericComponentWithCodeBehind<T> { [Parameter] public T Value { get; set; } }
@typeparam T
روش مقید کردن پارامترهای جنریک در کامپوننتها
از زمان دات نت 6 به بعد، امکان محدود کردن بازهی نوعهای قابل پذیرش T نیز میسر شدهاست:
@typeparam T where T : IMyInterface
روش مشخص کردن صریح نوع پارامترهای جنریک، در حین استفادهی از آنها
عموما نیازی به مشخص کردن نوع پارامترهای جنریک، در حین استفادهی از آنها نیست و کامپایلر بر اساس نوع مقدار ورودی، سعی خواهد کرد این نوع را به صورت خودکار تشخیص دهد؛ اما اگر به هر دلیلی چنین امری ممکن نبود و خطایی را دریافت کردید، میتوان نوع پارامتر جنریک را به صورت زیر مشخص کرد:
<MyGenericComponent Value="10" T="int"/>
روش پردازش پارامترهای جنریک در کامپوننتها
تا اینجا امکان پذیرش نوعهای جنریک را توسط استفاده کننده میسر کردیم؛ اما قسمت دیگر آن، نحوهی کار با نوع T، درون یک کامپوننت است:
using Microsoft.AspNetCore.Components; namespace BlazorGenericComponents.Pages; public partial class MyGenericComponentWithCodeBehind<T> { private string FormattedValue { set; get; } [Parameter] public T Value { get; set; } [Parameter] public string Format { get; set; } protected override void OnParametersSet() { FormattedValue = ConvertNumberToText(); } private string ConvertNumberToText() { return Value switch { short shortValue => shortValue.ToString(Format), ushort ushortValue => ushortValue.ToString(Format), int intValue => intValue.ToString(Format), uint uintValue => uintValue.ToString(Format), byte byteValue => byteValue.ToString(Format), sbyte sbyteValue => sbyteValue.ToString(Format), decimal decimalValue => decimalValue.ToString(Format), double doubleValue => doubleValue.ToString(Format), float floatValue => floatValue.ToString(Format), long longValue => longValue.ToString(Format), ulong ulongValue => ulongValue.ToString(Format), _ => string.Empty }; } }
و در آخر نمایش نهایی آن هم در فایل razor. متناظر، به این صورت خواهد بود:
@typeparam T @FormattedValue
مقدمه
Profiler یک ابزار گرافیکی برای ردیابی و نظارت بر کارآئی SQL Server است. امکان ردیابی اطلاعاتی در خصوص رویدادهای مختلف و ثبت این دادهها در یک فایل (با پسوند trc) یا جدول برای تحلیلهای آتی نیز وجود دارد. برای اجرای این ابزار مراحل زیر را انجام دهید:1- اصطلاحات
1-1- رویداد (Event):
یک رویداد، کاری است که توسط موتور بانک اطلاعاتی (Database Engine) انجام میشود. برای مثال هر یک از موارد زیر یک رویداد هستند.- متصل شدن کاربران (login connections) قطع شدن ارتباط یک login
- اجرای دستورات T-SQL، شروع و پایان اجرای یک رویه، شروع و پایان یک دستور در طول اجرای یک رویه، اجرای رویههای دور Remote Procedure Call
- باز شدن یک Cursor
- بررسی و کنترل مجوزهای امنیتی
1-2- کلاس رویداد (Event Class):
برای بکارگیری رویدادها در Profiler، از یک Event Class استفاده میکنیم. یک Event Class رویدادی است که قابلیت ردیابی دارد. برای مثال بررسی ورود و اتصال کاربران با استفاده از کلاس Audit Login قابل پیاده سازی است. هر یک از موارد زیر یک Event Class هستند.- SQL:BatchCompleted
- Audit Login
- Audit Logout
- Lock: Acquired
- Lock: Released
1-3- گروه رویداد (Event Category):
یک گروه رویداد شامل رویدادهایی است که به صورت مفهومی دسته بندی شده اند. برای مثال، کلیه رویدادهای مربوط به قفلها از جمله Lock: Acquired (بدست آوردن قفل) و Lock: Released (رها کردن قفل) در گروه رویداد Locks قرار دارند.1-4- ستون داده ای (Data Column):
یک ستون داده ای، خصوصیت و جزئیات یک رویداد را شامل میشود. برای مثال در یک Trace که رویدادهای Lock: Acquired را نظارت میکند، ستون Binary Data شامل شناسه (ID) یک صفحه و یا یک سطر قفل شده است و یا اینکه ستون Duration مدت زمان اجرای یک رویه را نمایش میدهد.1-5- الگو (Template):
یک الگو، مشخص کننده تنظیمات پیش گزیده برای یک Trace است، این تنظیمات شامل رویدادهایی است که نیاز دارید بر آنها نظارت داشته باشید. هنگامیکه یک Trace براساس یک الگو اجرا شود، رویدادهای مشخص شده، نظارت میشوند و نتیجه به صورت یک فایل یا جدول قابل مشاهده خواهد بود.1-6- ردیاب (Trace):
یک Trace دادهها را براساس رویدادهای انتخاب شده، جمع آوری میکند. امکان اجرای بلافاصله یک Trace برای جمع آوری اطلاعات با توجه به رویدادهای انتخاب شده و ذخیره کردن آن برای اجرای آتی وجود دارد.1-7- فیلتر (Filter):
هنگامی که یک Trace یا الگو ایجاد میشود، امکان تعریف شرایطی برای فیلتر کردن دادههای جمع آوری شده نیز وجود دارد. این کار باعث کاهش حجم دادههای گزارش شده میشود. برای مثال اطلاعات مربوط به یک کاربر خاص جمع آوری میشود و یا اینکه رشد یک بانک اطلاعاتی مشخص بررسی میشود.2- انتخاب الگو (Profiler Trace Templates)
از آنجائیکه اصولاً انتخاب Eventهای مناسب، کار سخت و تخصصی میباشد برای راحتی کار تعدادی Templateهای آماده وجود دارد، برای مثال TSQL_Duration تاکیدش روی مدت انجام کار است و یا SP_Counts در مواردی که بخواهیم رویههای ذخیره شده را بهینه کنیم استفاده میشود در جدول زیر به شرح هر یک پرداخته شده است:الگو | هدف |
Blank | ایجاد یک Trace کلی |
SP_Counts | ثبت اجرای هر رویه ذخیره شده برای تشخیص اینکه هر رویه چند بار اجرا شده است |
Standard | ثبت آمارهای کارائی برای هر رویه ذخیره شده و Queryهای عادی SQL که اجرا میشوند و عملیات ورود و خروج هر Login (پیش فرض) |
TSQL | ثبت یک لیست از همه رویههای ذخیره شده و Queryهای عادی SQL که اجرا میشوند ولی آمارهای کارائی را شامل نمیشود |
TSQL_Duration | ثبت مدت زمان اجرای هر رویه ذخیره شده و هر Query عادی SQL |
TSQL_Grouped | ثبت تمام loginها و logoutها در طول اجرای رویههای ذخیره شده و هر Query عادی SQL، شامل اطلاعاتی برای شناسائی برنامه و کاربری که درخواست را اجرا میکند |
TSQL_Locks | ثبت اطلاعات انسداد (blocking) و بن بست (deadlock) از قبیل blocked processes، deadlock chains، deadlock graphs,... . این الگو همچنین درخواستهای تمام رویههای ذخیره شده و تمامی دستورات هر رویه و هر Query عادی SQL را دریافت میکند |
TSQL_Replay | ثبت اجرای رویههای ذخیره شده و Queryهای SQL در یک SQL Instance و مهیا کردن امکان اجرای دوباره عملیات در سیستمی دیگر |
TSQL_SPs | ثبت کارائی برای Queryهای SQL، رویههای ذخیره شده و تمامی دستورات درون یک رویه ذخیره شده و نیز عملیات ورود و خروج هر Login |
Tuning | ثبت اطلاعات کارائی برای Queryهای عادی SQL و رویههای ذخیره شده و یا تمامی دستورات درون یک رویه ذخیره شده |
3- انتخاب رویداد (SQL Trace Event Groups)
رویدادها در 21 گروه رویداد دسته بندی میشوند که در جدول زیر لیست شده اند:گروه رویداد | هدف |
Broker | 13 رویداد برای واسطه سرویس (Service Broker) |
CLR | 1 رویداد برای بارگذاری اسمبلیهای CLR (Common Language Runtime) |
Cursors | 7 رویداد برای ایجاد، دستیابی و در اختیار گرفتن Cursor |
Database | 6 رویداد برای رشد/کاهش (grow/shrink) فایل های Data/Log همچنین تغییرات حالت انعکاس (Mirroring) |
Deprecation | 2 رویداد برای آگاه کردن وضعیت نابسامان درون یک SQL Instance |
Errors and Warnings | 16 رویداد برای خطاها، هشدارها و پیغامهای اطلاعاتی که ثبت شده است |
Full Text | 3 رویداد برای پیگیری یک شاخص متنی کامل |
Locks | 9 رویداد برای بدست آوردن، رها کردن قفل و بن بست (Deadlock) |
OLEDB | 5 رویداد برای درخواستهای توزیع شده و RPC (اجرای رویههای دور) |
Objects | 3 رویداد برای وقتی که یک شی ایجاد، تغییر یا حذف میشود |
Performance | 14 رویداد برای ثبت نقشه درخواستها (Query Plan) برای استفاده نقشه راهنما (Plan Guide) به منظور بهینه سازی کارائی درخواست ها، همچنین این گروه رویداد در خواستهای متنی کامل (full text) را ثبت میکند |
Progress Report | 10 رویداد برای ایجاد Online Index |
Query Notifications | 4 رویداد برای سرویس اطلاع رسان (Notification Service) |
Scans | 2 رویداد برای وقتی که یک جدول یا شاخص، پویش میشود |
Security Audit | 44 رویداد برای وقتی که مجوزی استفاده شود، جابجائی هویتی رخ دهد، تنظیمات امنیتی اشیائی تغییر کند،یک SQL Instance شروع و متوقف شود و یک Database جایگزین شود یا از آن پشتیبان گرفته شود |
Server | 3 رویداد برای Mount Tape، تغییر کردن حافظه سرور و بستن یک فایل Trace |
Sessions | 3 رویداد برای وقتی که Connectionها موجود هستند و یک Trace فعال میشود، همچنین یک Trigger و یک تابع دسته بندی(classification functions) مربوط به مدیریت منابع(resource governor) رخ دهد |
Stored Procedures | 12 رویداد برای اجرای یک رویه ذخیره شده و دستورات درون آن ، کامپایل مجدد و استفاده از حافظه نهانی (Cache) |
Transactions | 13 رویداد برای شروع، ذخیره ، تائید و لغو یک تراکنش |
TSQL | 9 رویداد برای اجرای Queryهای SQL و جستجوهای XQUERY (در دادههای XML) |
User Configurable | 10 رویداد که شما میتوانید پیکربندی کنید |
4- انتخاب ستونهای داده ای ( Data Columns)
اگرچه میتوان همهی 64 ستون داده ای ممکن را برای ردیابی انتخاب کرد ولیکن دادههای Trace شما زمانی مفید خواهند بود که اطلاعات ضروری را ثبت کرده باشید. برای مثال شماره ترتیب تراکنشها را، برای یک رویداد RPC:Completed میتوانید برگردانید، اما همه رویههای ذخیره شده مقادیر را تغییر نمیدهند بنابراین شماره ترتیب تراکنشها فضای بیهوده ای را مصرف میکند. بعلاوه همه ستونهای داده ای برای تمامی رویدادهای Trace معتبر نیستند. برای مثال Read ، Write ،CPU و Duration برای رویدادهای RPC:Starting و SQL:BatchStarting معتبر نیستند.ApplicationName، NTUserName، LoginName، ClientProcessID، SPID، HostName، LoginSID، NTDomainName و SessionLoginName ، مشخص میکنند چه کسی و از چه منشاء دستور را اجرا کرده است.
ستون SessionLoginName معمولاً نام Login ای که از آن برای متصل شدن به SQL Instance استفاده شده است را نشان میدهد. در حالیکه ستون LoginName نام کاربری را که دستور را اجرا میکند نشان میدهد (EXECUTE AS). ستون ApplicationName خالی است مگر اینکه در ConnectionString برنامه کاربردیمان این خصوصیت (Property) مقداردهی شده باشد. ستون StartTime و EndTime زمان سرحدی برای هر رویداد را ثبت میکند این ستونها بویژه در هنگامی که به عملیات Correlate نیاز دارید مفید هستند.
5- بررسی چند سناریو نمونه
• یافتن درخواست هائی (Queries) که بدترین کارایی را دارا هستند.
برای ردیابی درخواستهای ناکارا، از رویداد RPC:Completed از دسته Stored Procedure و رویداد SQL:BatchCompleted از دسته TSQL استفاده میشود.• نظارت بر کارایی رویه ها
برای ردیابی کارائی رویه ها، از رویدادهای SP:Starting، SP:Completed، SP:StmtCompleted و SP:StmtStaring از کلاس Stored Procedure و رویدادهای SQL:BatchStarting ، SQL:BatchCompleted از کلاس TSQL استفاده میشود.• نظارت بر اجرای دستورات T-SQL توسط هر کاربر
برای ردیابی دستوراتی که توسط یک کاربر خاص اجرا میشود، نیاز به ایجاد یک Trace برای نظارت بر رویدادهای کلاسهای Sessions، ExistingConnection و TSQL داریم همچنین لازم است نام کاربر در قسمت فیلتر و با استفاده از DBUserName مشخص شود.• اجرا دوباره ردیاب (Trace Replay)
این الگو معمولاً برای debugging استفاده میشود برای این منظور از الگوی Replay استفاده میشود. در ضمن امکان اجرای دوباره عملیات در سیستمی دیگر با استفاده از این الگو مهیا میشود.• ابزار Tuning Advisor (راهنمای تنظیم کارائی)
این ابزاری برای تحلیل کارائی یک یا چند بانک اطلاعاتی و تاثیر عملکرد آنها بر بار کاری (Workload) سرویس دهنده است. یک بار کاری مجموعه ای از دستورات T-SQL است که روی بانک اطلاعاتی اجرا میشود. بعد از تحلیل تاثیر بارکاری بر بانک اطلاعاتی، Tuning Advisor توصیه هائی برای اضافه کردن، حذف و یا تغییر طراحی فیزیکی ساختار بانک اطلاعاتی ارائه میدهد این تغییرات ساختاری شامل پیشنهاد برای تغییر ساختاری موارد Clustered Indexes، Nonclustered Indexes، Indexed View و Partitioning است.برای ایجاد بارکاری میتوان از یک ردیاب تهیه شده در SQL Profiler استفاده کرد برای این منظور از الگوی Tuning استفاده میشود و یا رویدادهای RPC:Completed، SQL:BatchCompleted و SP:StmtCompleted را ردیابی نمائید.
• ترکیب ابزارهای نظارتی (Correlating Performance and Monitoring Data)
یک Trace برای ثبت اطلاعاتی که در یک SQL Instance رخ میدهد، استفاده میشود. System Monitor برای ثبت شمارندههای کارائی(performance counters) استفاده میشود و همچنین از منابع سخت افزاری و اجزای دیگر که روی سرور اجرا میشوند، تصاویری فراهم میکند. توجه شود که در مورد Correlating یک فایل ردیاب (trace file) و یک Counter Log (ابزار Performance )، ستون داده ای StartTime و EndTime باید انتخاب شود، برای این کار از منوی File گزینه Import Performance Data انتخاب میشود.• جستجوی علت رخ دادن یک بن بست
برای ردیابی علت رخ دادن یک بن بست، از رویدادهای RPC:Starting، SQLBatchStarting از دسته Stored Procedure و رویدادهای Deadlock graph، Lock:Deadlock و Lock:Deadlock Chain از دسته Locks استفاده میشود. ( در صورتی که نیاز به یک ارائه گرافیکی دارید از Deadlock graph استفاده نمائید، خروجی مطابق تصویر زیر میشود).
5-1- ایجاد یک Trace
1- Profiler را اجرا کنید از منوی File گزینه New Trace را انتخاب کنید و به SQL Instance مورد نظرتان متصل شوید.2- مطابق تصویر زیر برای Trace یک نام و الگو و تنظیمات ذخیره سازی فایل را مشخص کنید.
3- بر روی قسمت Events Selection کلیک نمائید.
4- مطابق تصویر زیر رویدادها و کلاس رویدادها را انتخاب کنید، ستونهای TextData، NTUserName، LoginName، CPU،Reads،Writes، Duration، SPID، StartTime، EndTime، BinaryData، DataBaseName، ServerName و ObjectName را انتخاب کنید.
5- روی Column Filters کلیک کنید و مطابق تصویر زیر برای DatabaseName فیلتری تنظیم کنید.
6- روی Run کلیک کنید. تعدادی Query و رویه ذخیره شده مرتبط با پایگاه داده AdventureWorks اجرا کنید .
5-2- ایجاد یک Counter Log
برای ایجاد یک Counter Log مراحل زیر را انجام دهید:1- ابزار Performance را اجرا کنید (برای این کار عبارتPerfMon را در قسمت Run بنویسید).
2- در قسمت Counter Logs یک log ایجاد کنید.
3- روی Add Counters کلیک کرده و مطابق تصویر موارد زیر را انتخاب کنید.
Select counters from list | Performance Object |
Output Queue Length | Network Interface |
% Processor Time | Processor |
Processor Queue Length | System |
Buffer Manager:Page life expectancy | SQLServer |
4- روی Ok کلیک کنید تا Counter Log ذخیره شود سپس روی آن راست کلیک کرده و آنرا Start کنید.
5-3- ترکیب ابزارهای نظارتی (Correlating SQL Trace and System Monitor Data)
1- Profiler را اجرا کنید از منوی File گزینه Open و سپس Trace File را انتخاب کنید فایل trc را که در گام اول ایجاد کردید، باز نمائید.2- از منوی File گزینه Import Performance Data را انتخاب کنید و فایل counter log را که در مرحله قبل ایجاد کردید انتخاب کنید.
نکته: اطلاعات فایل trc را میتوان درون یک جدول وارد کرد، بدین ترتیب میتوان آنالیز بیشتری داشت به عنوان مثال دستورات زیر این عمل را انجام میدهند.
SELECT * INTO dbo.BaselineTrace FROM fn_trace_gettable(' c:\performance baseline.trc ', default);
Image Annotations
ایجاد دیتابیس
ابتدا یک دیتابیس به نام Coordinates ایجاد کنید و سپس جدول زیر رو ایجاد کنید
USE [Coordinates] GO CREATE TABLE [dbo].[Coords2]( [top] [int] NULL, [left] [int] NULL, [width] [int] NULL, [height] [int] NULL, [text] [nvarchar](50) NULL, [id] [uniqueidentifier] NULL, [editable] [bit] NULL ) ON [PRIMARY] GO
ایجاد کلاس Coords برای خواندن و ذخیره اطلاعات
public class Coords { public string top; public string left; public string width; public string height; public string text; public string id; public string editable; public Coords(string top, string left, string width, string height, string text, string id, string editable) { this.top = top; this.left = left; this.width = width; this.height = height; this.text = text; this.id = id; this.editable = editable; } }
1-GetDynamicContext
این متد در زمان لود اطلاعات از دیتابیس استفاده میشود (وقتی که postback صورت میگیرد)
[WebMethod] public static List<Coords> GetDynamicContext(string entryId, string entryName) { List<Coords> CoordsList = new List<Coords>(); string connect = "Connection String"; using (SqlConnection conn = new SqlConnection(connect)) { string query = "SELECT [top], [left], width, height, text, id, editable FROM Coords2"; using (SqlCommand cmd = new SqlCommand(query, conn)) { conn.Open(); using (SqlDataReader reader=cmd.ExecuteReader()) { while (reader.Read()) { CoordsList.Add(new Coords(reader["top"].ToString(), reader["left"].ToString(), reader["width"].ToString(), reader["height"].ToString(), reader["text"].ToString(), reader["id"].ToString(), reader["editable"].ToString())); } } conn.Close(); } } return CoordsList; }
2,3 -SaveCoordsو DeleteCoords
این دو متد هم واسه ذخیره و حذف میباشند که نکته خاصی ندارند و خودتون بهینه اش کنید(در فایل ضمیمه موجودند)
تغییر فایل jquery.annotate.js جهت فراخوانی وب سرویس ها
فقط لازمه که سه قسمت زیر رو در فایل اصلی تغییر بدید
$.fn.annotateImage.ajaxLoad = function (image) { ///<summary> ///Loads the annotations from the "getUrl" property passed in on the /// options object. ///</summary> $.ajax({ type: "POST", contentType: "application/json; charset=utf-8", url: "Default.aspx/GetDynamicContext", data: "{'entryId': '" + 1 + "','entryName': '" + 2 + "'}", dataType: "json", success: function (msg) { image.notes = msg.d; $.fn.annotateImage.load(image); } });
};
$.fn.SaveCoords = function (note) { $.ajax({ type: "POST", contentType: "application/json; charset=utf-8", url: "Default.aspx/SaveCoords", data: "{'top': '" + note.top + "','left': '" + note.left + "','width': '" + note.width + "','height': '" + note.height + "','text': '" + note.text + "','id': '" + note.id + "','editable': '" + note.editable + "'}", dataType: "json", success: function (msg) { note.id = msg.d; } });
};
$.fn.annotateView.prototype.edit = function () {
///<summary>
///Edits the annotation.
///</summary>
if (this.image.mode == 'view') {
this.image.mode = 'edit';
var annotation = this;
// Create/prepare the editable note elements
var editable = new $.fn.annotateEdit(this.image, this.note);
$.fn.annotateImage.createSaveButton(editable, this.image, annotation);
// Add the delete button
var del = $('<a>حذف</a>');
del.click(function () {
var form = $('#image-annotate-edit-form form');
$.fn.annotateImage.appendPosition(form, editable)
if (annotation.image.useAjax) {
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: "Default.aspx/DeleteCoords",
// url: annotation.image.deleteUrl,
// data: form.serialize(),
data: "{'id': '" + editable.note.id + "'}",
dataType: "json",
success: function (msg) {
// image.notes = msg.d;
// $.fn.annotateImage.load(image);
},
error: function (e) { alert("An error occured deleting that note.") }
});
}
annotation.image.mode = 'view';
editable.destroy();
annotation.destroy();
});
editable.form.append(del);
$.fn.annotateImage.createCancelButton(editable, this.image);
}
};
var connection = new SqliteConnection("Data Source=:memory:"); connection.Open(); var createCommand = connection.CreateCommand(); createCommand.CommandText = @" CREATE TABLE persian_letter ( value TEXT ); INSERT INTO persian_letter VALUES ('ا'),('ب'),('پ'),('ت'),('ث'),('ج'),('چ'),('ح'),('خ'),('د'),('ذ'),('ر'),('ز'),('ژ'),('س'),('ش'), ('ص'),('ض'),('ط'),('ظ'),('ع'),('غ'),('ف'),('ق'),('ک'),('گ'),('ل'),('م'),('ن'),('و'),('ه'),('ی'); "; createCommand.ExecuteNonQuery();
var queryCommand = connection.CreateCommand(); queryCommand.CommandText = @" SELECT value FROM persian_letter order by value "; var reader = queryCommand.ExecuteReader(); var sortedDbItems = new List<string>(); while (reader.Read()) { sortedDbItems.Add($"{reader["value"]}"); }
همانطور که ملاحظه میکنید، مرتب سازی حروف فارسی در اینجا به صورت پیشفرض کار نمیکند. علت اینجا است که روش پیشفرض مرتب سازی حروف در SQLite، بر اساس کد اسکی حروف است و فقط در مورد حروف ASCII از A تا Z درست کار میکند.
امکان تعریف Collation سفارشی در SQLite
در بانکهای اطلاعاتی، قابلیتی که مستقیما بر روی نحوهی جستجو و همچنین مرتب سازی حروف تاثیر میگذارد، Collation نام دارد و در SQLite برخلاف بسیاری از بانکهای اطلاعاتی دیگر، امکان تعریف Collation سفارشی نیز وجود دارد و برای این منظور باید یک function pointer را در اختیار آن قرار داد تا از آن در سمت بانک اطلاعاتی جهت مرتب سازی و جستجوی حروف استفاده کند.
خوشبختانه پروژهی Microsoft.Data.Sqlite امکان تبدیل یک managed delegate دات نتی را به یک function pointer مخصوص SQLite، میسر میکند. به عبارتی SQLite کدهای دات نتی را در حین انجام محاسبات خود اجرا خواهد کرد و این اجرا به صورتی نیست که ابتدا کل اطلاعات، به سمت برنامهی کلاینت منتقل شود و سپس در این سمت، در حافظه، عملیاتی بر روی آن صورت گیرد. کل عملیات در سمت بانک اطلاعاتی مدیریت میشود.
روش تعریف یک Collation جدید هم در اینجا بسیار سادهاست:
connection.CreateCollation("PersianCollationNoCase", (x, y) => string.Compare(x, y, ignoreCase: true));
SELECT value FROM persian_letter order by value COLLATE PersianCollationNoCase
تعریف Collation سفارشی غیرحساس به «ی و ک» !
این مورد شاید یکی از آرزوهای توسعه دهندگان SQL Server باشد! اما با SQLite به سادگی زیر قابل تعریف و مدیریت است:
connection.CreateCollation("PersianCollationNoCaseYekeInsensitive", (x, y) => string.Compare(x.ApplyCorrectYeKe(), y.ApplyCorrectYeKe(), ignoreCase: true));
در یک چنین حالتی اگر اطلاعاتی را به همراه «ی و ک» فارسی و یا عربی ثبت کنیم:
CREATE TABLE persian_letter ( value TEXT ); INSERT INTO persian_letter VALUES ('ی'),('ک');
SELECT count() FROM persian_letter WHERE value = 'ی' COLLATE PersianCollationNoCaseYekeInsensitive
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: SQLitePersianCollation.zip
DbProviderFactory در Net Framework 2.0. ارائه شده است.برای درک و چگونگی استفاده از DBProviderFactory مثالی را بررسی مینماییم.
ابتدا کد زیر را درون یک فرم کپی نمایید:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Data.Common; namespace DBFactory { public partial class Form1 : Form { private string _MySQLProvider = "MySql.Data.MySqlClient"; private string _SQLProvider="System.Data.SqlClient"; private string _OracleProvider ="System.Data.OracleClient"; private DbProviderFactory _DbProviderFactory; private DbConnection _DbConnection = null; private DbCommand _DbCommand = null; private DbDataAdapter _DbDataAdapter = null; public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { try { string _SQLconnectionstring = "Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=Test;Data Source=FARHAD-PC"; string _Oracleconnectionstring = "Data Source=ServiceName;User Id=Username;Password=Password"; _DbProviderFactory = DbProviderFactories.GetFactory(_SQLProvider); _DbConnection = _DbProviderFactory.CreateConnection(); _DbConnection.ConnectionString = _SQLconnectionstring; _DbConnection.Open(); if (_DbConnection.State == ConnectionState.Closed) { MessageBox.Show("اتصال با دیتابیس برقرار نشده است"); } else { MessageBox.Show("اتصال با دیتابیس با موفقیت بر قرار شده است"); } } catch (System.Exception excep) { MessageBox.Show(excep.Message.ToString()); } } } }
برای استفاد از DBProviderFactory میبایست از فضای نامی System.Data.Common استفاده نمایید. بعد از اعلان کلاس فرم تعدادی آبجکت تعریف شده است، که سه آبجکت ابتدایی آن، بیانگر Provider دیتابیسهای MySQL،SQLSERVER و Oracle میباشد:
private string _MySQLProvider = "MySql.Data.MySqlClient"; private string _SQLProvider="System.Data.SqlClient"; private string _OracleProvider ="System.Data.OracleClient";
_DbProviderFactory = DbProviderFactories.GetFactory("System.Data.SqlClient");
در کد بالا، Provider، دیتابیس SQLSERVER به DbProviderFactory به عنوان ورودی داده شده است، بنابراین آبجکتهای مربوط به دیتابیس SQL Server ایجاد و در اختیار شما قرار میگیرد.
اگر به نام فضای نامی System.Data.Common توجه نمایید،از کلمه Common استفاده شده است و منظور این است که تمامی کلاسهایی را که این فضای نامی ارائه میدهد، در هر دیتابیسی قابل استفاده میباشد. برای تشخیص، کلاسهای مربوط به این فضای نامی نیز در ابتدای نام آنها از دو حرف DB استفاده شده است. تمامی کلاسهای زیر در فضای نامی System.Data.Common قابل ارائه و استفاده میباشد:
DbCommand DbCommandBuilder DbConnection DbDataAdapter DbDataReader DbException DbParameter DbTransaction
جهت اطلاع: ممکن است سئوالی در ذهن شما ایجاد شود که دات نت چگونه براساس نام Provider نوع دیتابیس را تشخیص میدهد؟
جواب: زمانی که دیتابیسهای مختلف روی سیستم شما نصب میشود، Providerهای مربوط به هر دیتابیس درون فایل Machine.config که مربوط به دات نت میباشد، درج میشود. و دات نت براساس اطلاعات مربوط به همین فایل آبجکتهای دیتابیس را ایجاد مینماید.
امیدوارم مطلب فوق مفید واقع شود.
ابتدا لازم است Entity framework را توسط Nuget packages manager دانلود نمایید. سپس به پروژهی خود یک فولدر جدید را به نام Models و درون آن ابتدا یک کلاس را به نام User اضافه کرده و بدین صورت خواهیم داشت :
using System; namespace Console1.Models { public enum UserType { Admin, Ordinary } public class User { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public UserType Type { get; set; } } }
UserType نیز کاملا مشخص است؛ هر User نقش Admin یا Ordinary را میتواند داشته باشد.
نوبت به نوشتن اینترفیس IUser میرسد. در همین پوشهای که قرار داریم، آن را پیاده سازی مینماییم:
namespace Console1.Models { public interface IUser { int UserId { get; set; } User User { get; set; } } }
هر entity که با User ارتباط دارد، باید اینترفیس فوق را پیاده سازی نماید. حال یک کلاس دیگر را به نام Post در همین پوشه درست کرده و بدین صورت پیاده سازی مینماییم.
using System.ComponentModel.DataAnnotations.Schema; namespace Console1.Models { public class Post : IUser { public int Id { get; set; } public string Context { get; set; } public int UserId { get; set; } [ForeignKey(nameof(UserId))] public User User { get; set; } } }
واضح است که relation از نوع one to many برقرار است و هر User میتواند n تا Post داشته باشد.
خوب تا اینجا کافیست و میخواهیم مدلهای خود را با استفاده از EF به Context معرفی کنیم. میتوانیم در همین پوشه کلاسی را به نام Context ساخته و بصورت زیر بنویسیم
using System.Data.Entity; namespace Console1.Models { public class Context : DbContext { public Context() : base("Context") { } public DbSet<User> Users { get; set; } public DbSet<Post> Posts { get; set; } } }
در اینجا مشخص کردهایم که دو Dbset از نوع User و Post را داریم. بدین معنا که EF دو table را برای ما تولید خواهد کرد. همچنین نام کلید رشتهی اتصالی به دیتابیس خود را نیز، Context معرفی کردهایم.
خوب تا اینجا قسمت اول پروژهی خود را تکمیل کردهایم. الان میتوانیم با استفاده از Migration دیتابیس خود را ساخته و همچنین رکوردهایی را بدان اضافه کنیم. در Package Manager Console خود دستور زیر را وارد نمایید:
enable-migrations
به صورت خودکار پوشهای به نام Migrations ساخته شده و درون آن Configuration.cs قرار میگیرد که آن را بدین صورت تغییر میدهیم:
namespace Console1.Migrations { using Models; using System.Data.Entity.Migrations; internal sealed class Configuration : DbMigrationsConfiguration<Console1.Models.Context> { public Configuration() { AutomaticMigrationsEnabled = true; } protected override void Seed(Console1.Models.Context context) { context.Users.AddOrUpdate(x => x.Id, new User { Id = 1, Name = "aaa", Age = 30, Type = UserType.Admin }, new User { Id = 2, Name = "bbb", Age = 20, Type = UserType.Ordinary }, new User { Id = 3, Name = "ccc", Age = 25, Type = UserType.Ordinary } ); context.Posts.AddOrUpdate(x => x.Id, new Post { Context = "ccc 1", UserId = 3 }, new Post { Context = "bbb 1", UserId = 2 }, new Post { Context = "bbb 2", UserId = 2 }, new Post { Context = "aaa 1", UserId = 1 }, new Post { Context = "bbb 3", UserId = 2 }, new Post { Context = "ccc 2", UserId = 3 }, new Post { Context = "ccc 3", UserId = 3 } ); context.SaveChanges(); } } }
در متد seed، رکوردهای اولیه را به شکل فوق وارد کرده ایم (رکوردها فقط به منظور تست میباشند*). در کنسول دستور Update-database را ارسال کرده، دیتابیس تولید خواهد شد.
قطعا مراحل بالا کاملا بدیهی بوده و نوشتن آنها بدین دلیل بوده که در Repository که الان میخواهیم شروع به نوشتنش کنیم به مدلهای فوق نیاز داریم تا بصورت کاملا عملی با مراحل کار آشنا شویم.
حال میخواهیم به پیاده سازی بخش اصلی این مقاله یعنی repository که از Row Level Security پشتیبانی میکند بپردازیم. در ریشهی پروژهی خود پوشهای را به نام Repository ساخته و درون آن کلاسی را به نام GenericRepository میسازیم. پروژهی شما هم اکنون باید ساختاری شبیه به این را داشته باشد.
GenericRepository.cs را اینگونه پیاده سازی مینماییم
using Console1.Models; using System; using System.Linq; using System.Linq.Dynamic; using System.Linq.Expressions; namespace Console1.Repository { public interface IGenericRepository<T> { IQueryable<T> CustomizeGet(Expression<Func<T, bool>> predicate); void Add(T entity); IQueryable<T> GetAll(); } public class GenericRepository<TEntity, DbContext> : IGenericRepository<TEntity> where TEntity : class, new() where DbContext : Models.Context, new() { private DbContext _entities = new DbContext(); public IQueryable<TEntity> CustomizeGet(Expression<Func<TEntity, bool>> predicate) { IQueryable<TEntity> query = _entities.Set<TEntity>().Where(predicate); return query; } public void Add(TEntity entity) { int userId = Program.UserId; // یوزد آی دی بصورت فیک ساخته شده // اگر از آیدنتیتی استفاده میکنید میتوان آی دی و هر چیز دیگری که کلیم شده را در اختیار گرفت if (typeof(IUser).IsAssignableFrom(typeof(TEntity))) { ((IUser)entity).UserId = userId; } _entities.Set<TEntity>().Add(entity); } public IQueryable<TEntity> GetAll() { IQueryable<TEntity> result = _entities.Set<TEntity>(); int userId = Program.UserId; // یوزد آی دی بصورت فیک ساخته شده // اگر از آیدنتیتی استفاده میکنید میتوان آی دی و هر چیز دیگری که کلیم شده را در اختیار گرفت if (typeof(IUser).IsAssignableFrom(typeof(TEntity))) { User me = _entities.Users.Single(c => c.Id == userId); if (me.Type == UserType.Admin) { return result; } else if (me.Type == UserType.Ordinary) { string query = $"{nameof(IUser.UserId).ToString()}={userId}"; return result.Where(query); } } return result; } public void Commit() { _entities.SaveChanges(); } } }
1) یک اینترفیس Generic را به نام IGenericRepository داریم که کلاس GenericRepository قرار است آن را پیاده سازی نماید.
2) این اینترفیس شامل متدهای CustomizeGet است که فقط یک predicate را گرفته و خیلی مربوط به این مقاله نیست (صرفا جهت اطلاع). اما متد Add و GetAll بصورت مستقیم قرار است هدف row level security را برای ما انجام دهند.
3) کلاس GenericRepository دو Type عمومی را به نام TEntity و DbContext گرفته و اینترفیس IGenericRepository را پیاده سازی مینماید. همچنین صریحا اعلام کردهایم TEntity از نوع کلاس و DbContext از نوع Context ایی است که قبلا نوشتهایم.
4) پیاده سازی متد CustomizeGet را مشاهده مینمایید که کوئری مربوطه را ساخته و بر میگرداند.
5) پیاده سازی متد Add بدین صورت است که به عنوان پارامتر، TEntity را گرفته (مدلی که قرار است save شود). بعد مشاهد میکنید که من به صورت hard code به UserId مقدار دادهام. قطعا میدانید که برای این کار به فرض اینکه از Asp.net Identity استفاده میکنید، میتوانید Claim آن Id کاربر Authenticate شده را بازگردانید.
با استفاده از IsAssignableFrom مشخص کردهایم که آیا TEntity یک Typeی از IUser را داشته است یا خیر؟ در صورت true بودن شرط، UserId را به TEntity اضافه کرده و بطور مثال در Serviceهای خود نیازی به اضافه کردن متوالی این فیلد نخواهید داشت و در مرحلهی بعد نیز آن را به entity_ اضافه مینماییم.
مشاهده مینمایید که این متد به قدری انعطاف پذیری دارد که حتی مدلهای مختلف به صورت کاملا یکپارچه میتوانند از آن استفاده نمایند.
6) به جالبترین متد که GetAll میباشد میرسیم. ابتدا کوئری را از آن Entity ساخته و در مرحلهی بعد مشخص مینماییم که آیا TEntity یک Typeی از IUser میباشد یا خیر؟ در صورت برقرار بودن شرط، User مورد نظر را یافته در صورتیکه Typeی از نوع Admin داشت، همهی مجموعه را برخواهیم گرداند (Admin میتواند همهی پستها را مشاهده نماید) و در صورتیکه از نوع Ordinary باشد، با استفاده از dynamic linq، کوئری مورد نظر را ساخته و شرط را ایجاد میکنیم که UserId برابر userId مورد نظر باشد. در این صورت بطور مثال همهی پستهایی که فقط مربوط به user خودش میباشد، برگشت داده میشود.
نکته: برای دانلود dynamic linq کافیست از طریق nuget آن را جست و جو نمایید: System.Linq.Dynamic
و اگر هم از نوع IUser نبود، result را بر میگردانیم. بطور مثال فرض کنید مدلی داریم که قرار نیست security روی آن اعمال شود. پس کوئری ساخته شده قابلیت برگرداندن همهی رکوردها را دارا میباشد.
7) متد Commit هم که پرواضح است عملیات save را اعمال میکند.
قبلا در قسمت Seed رکوردهایی را ساخته بودیم. حال میخواهیم کل این فرآیند را اجرا نماییم. Program.cs را از ریشهی پروژهی خود باز کرده و اینگونه تغییر میدهیم:
using System; using Console1.Models; using Console1.Repository; using System.Collections.Generic; using System.Linq; namespace Console1 { public class Program { public static int UserId = 1; //fake userId static void Main() { GenericRepository<Post, Context> repo = new GenericRepository<Post, Context>(); List<Post> posts = repo.GetAll().ToList(); foreach (Post item in posts) Console.WriteLine(item.Context); Console.ReadKey(); } } }
همانطور که ملاحظه میکنید، UserId به صورت fake ساخته شده است. آن چیزی که هم اکنون در دیتابیس رفته، بدین صورت است که UserId = 1 برابر Admin و بقیه Ordinary میباشند. در متد Main برنامه، یک instance از GenericRepository را گرفته و بعد با استفاده از متد GetAll و لیست کردن آن، همهی رکوردهای مورد نظر را برگردانده و سپس چاپ مینماییم. در صورتی که UserId برابر 1 باشد، توقع داریم که همهی رکوردها بازگردانده شود:
حال کافیست مقدار userId را بطور مثال تغییر داده و برابر 2 بگذاریم. برنامه را اجرا کرده و مشاهد میکنیم که با تغییر یافتن userId، عملیات مورد نظر متفاوت میگردد و به صورت زیر خواهد شد:
میبینید که تنها با تغییر userId رفتار عوض شده و فقط Postهای مربوط به آن User خاص برگشت داده میشود.
از همین روش میتوان برای طراحی Repositoryهای بسیار پیچیدهتر نیز استفاده کرد و مقدار زیادی از validationها را به طور مستقیم بدان واگذار نمود!
دانلود کدها در Github
یکی از اشتباهاتی که همهی ما کم و بیش به آن دچار هستیم ایجاد کلاسهایی هستند که «زیاد میدانند». اصطلاحا به آنها God Classes هم میگویند و برای نمونه، پسوند یا پیشوند Util دارند. این نوع کلاسها اصل SRP را زیر سؤال میبرند (Single responsibility principle). برای مثال یک فایل ایجاد میشود و داخل آن از انواع و اقسام متدهای «کمکی» کار با دیتابیس تا رسم نمودار تا تبدیل تاریخ میلادی به شمسی و ... در طی بیش از 10 هزار سطر قرار میگیرند. یا برای مثال گروه بندیهای خاصی را در این یک فایل از طریق کامنتهای نوشته شده برای قسمتهای مختلف میتوان یافت. Refactoring مرتبط با این نوع کلاسهایی که «زیاد میدانند»، تجزیه آنها به کلاسهای کوچکتر، با تعداد وظیفهی کمتر است.
به عنوان نمونه کلاس CustomerService زیر، دو گروه کار متفاوت را انجام میدهد. ثبت و بازیابی اطلاعات ثبت نام یک مشتری و همچنین محاسبات مرتبط با سفارشات مشتریها:
using System;
using System.Collections.Generic;
namespace Refactoring.Day8.RemoveGodClasses.Before
{
public class CustomerService
{
public decimal CalculateOrderDiscount(IEnumerable<string> products, string customer)
{
// do work
throw new NotImplementedException();
}
public bool CustomerIsValid(string customer, int order)
{
// do work
throw new NotImplementedException();
}
public IEnumerable<string> GatherOrderErrors(IEnumerable<string> products, string customer)
{
// do work
throw new NotImplementedException();
}
public void Register(string customer)
{
// do work
}
public void ForgotPassword(string customer)
{
// do work
}
}
}
بهتر است این دو گروه، به دو کلاس مجزا بر اساس وظایفی که دارند، تجزیه شوند. به این ترتیب نگهداری این نوع کلاسهای کوچکتر در طول زمان سادهتر خواهند شد:
using System;
using System.Collections.Generic;
namespace Refactoring.Day8.RemoveGodClasses.After
{
public class CustomerOrderService
{
public decimal CalculateOrderDiscount(IEnumerable<string> products, string customer)
{
// do work
throw new NotImplementedException();
}
public bool CustomerIsValid(string customer, int order)
{
// do work
throw new NotImplementedException();
}
public IEnumerable<string> GatherOrderErrors(IEnumerable<string> products, string customer)
{
// do work
throw new NotImplementedException();
}
}
}
namespace Refactoring.Day8.RemoveGodClasses.After
{
public class CustomerRegistrationService
{
public void Register(string customer)
{
// do work
}
public void ForgotPassword(string customer)
{
// do work
}
}
}