مطالب
تحلیل و بررسی ده روش آسیب پذیری نرم افزار بر اساس متدولوژی OWASP - قسمت اول SQL Injection
در این سری از مقالات، ده روش برتر آسیب پذیری نرم افزار بر اساس متدولوژی OWASP مورد بررسی قرار میگیرد. یادگیری این روش‌ها منحصر به زبان برنامه نویسی خاصی نیست و رعایت این نکات برای برنامه نویسانی که قصد نوشتن کدی امن دارند، توصیه میشود. کلمه‌ی OWASP مخفف عبارت Open Web Application Security Protocol Project می‌باشد. در واقع OWASP یک متدولوژی و پروژه‌ی متن باز است که معیارهایی را برای ایمن سازی نرم افزار مورد بررسی قرار میدهد.



آسیب پذیری SQL Injection یا به اختصار SQLi

تزریق SQL، یکی از قدیمی ترین، شایع‌ترین و مخرب‌ترین آسیب پذیری‌ها، برای برنامه‌ها می‌باشد و در صورت برقراری شرایط مناسب جهت حمله و با اعمال نفوذ، از طریق تزریق SQL ، مهاجم میتواند با دور زدن فرآیندهای اعتبارسنجی و احراز هویت یک برنامه، به تمامی محتوای پایگاه داده‌ی آن و گاها کنترل سرور، دسترسی پیدا کند. این حمله برای افزودن، ویرایش و حذف رکوردهای یک پایگاه داده مبتنی بر SQL انجام میشود.


عملکرد SQL Injection

برای اجرای SQLهای مخرب در برنامه‌هایی که از پایگاه‌های داده‌ی مبتنی بر SQL مانند (SQL Server ،MySQL ،PostgreSQL ،Oracle و ...) استفاده میکنند، هکر یا مهاجم در اولین گام باید به دنبال ورودی‌هایی در برنامه باشد که درون یک درخواست SQL قرار گرفته باشند (مانند صفحات لاگین، ثبت نام، جستجو و ...).

کد زیر را در نظر بگیرید:

# Define POST variables
uname = request.POST['username']
passwd = request.POST['password']

# SQL query vulnerable to SQLi
sql = "SELECT id FROM users WHERE username='" + uname + "' AND password='" + passwd + "'"

# Execute the SQL statement
database.execute(sql)
در این کد، موارد امنیتی برای جلوگیری از تزریق SQL تعبیه نشده‌است و هکر با اندکی دستکاری پارامترهای ارسالی میتواند نتایج دلخواهی را برای نفوذ، بدست بیاورد. قسمتی از کد که دستور SQL را اجرا میکند و پارامترهای ورودی از کاربر را در خود جای میدهد، بصورت نا امنی کدنویسی شده‌است.

اکنون ورودی password را برای نفوذ، تست میکنیم. مهاجم بدون داشتن نام کاربری، قصد دور زدن احراز هویت را دارد. بجای password عبارت زیر را قرار میدهد:

password' OR 1=1  

در نهایت در بانک اطلاعاتی دستور زیر اجرا میشود:

 SELECT id FROM users WHERE username='username' AND password=  'password' OR 1=1'

میدانیم که 1=1 است. پس بدون در نظر گرفتن اینکه شما برای username و password چه چیزی را وارد نمودید، عبارت درست در نظر گرفته میشود:

شرط اول   and   شرط دوم =
نتیجه  or 1=1
چون 1=1 است 
همیشه شرط کوئری درست خواهد بود


معمولا در بانک اطلاعاتی، اولین کاربری که وارد میکنند Administrator برنامه می‌باشد. پس به احتمال قوی شما میتوانید با مجوز ادمین به برنامه وارد شوید. البته میتوان با دانستن تنها نام کاربری هم به‌راحتی با گذاشتن در قسمت username بدون دانستن password، به برنامه وارد شد؛ زیرا میتوان شرط چک کردن password را کامنت نمود:

-- MySQL, MSSQL, Oracle, PostgreSQL, SQLite
' OR '1'='1' --
' OR '1'='1' /*
-- MySQL
' OR '1'='1' #
-- Access (using null characters)
' OR '1'='1' %00
' OR '1'='1' %16




ابزارهایی برای تست آسیب پذیری SQLi

1) برنامه‌ی DNTProfiler

2) اسکنر اکانتیکس

3) sqlmap
4) روش دستی که بهترین نتیجه را دارد و نیاز به تخصص دارد.


چگونه از SQL Injection جلوگیری کنیم

1) روی داده‌هایی که از کاربر دریافت میگردد، اعتبار سنجی سمت کلاینت و سرور انجام شود. اگر فقط به اعتبارسنجی سمت کلاینت اکتفا کنید، هکر به‌راحتی با استفاده از  پروکسی، داده‌ها را تغییر می‌دهد. ورودی‌ها را فیلتر و پاکسازی و با لیست سفید یا سیاه بررسی کنید ( ^^^^ ).

2) از کوئری‌هایی که بدون استفاده از پارامتر از کاربر ورودی گرفته و درون یک درخواستSQL قرار میگیرند، اجتناب کنید:

[HttpGet]
        [Route("nonsensitive")]
        public string GetNonSensitiveDataById()
        {
            using (SqlConnection connection = new SqlConnection(_configuration.GetValue<string>("ConnectionString")))
            {
                connection.Open();
                SqlCommand command = new SqlCommand($"SELECT * FROM NonSensitiveDataTable WHERE Id = {Request.Query["id"]}", connection);
                using (var reader = command.ExecuteReader())
                {
                    if (reader.Read())
                    {
                        string returnString = string.Empty;
                        returnString += $"Name : {reader["Name"]}. ";
                        returnString += $"Description : {reader["Description"]}";
                        return returnString;
                    }
                    else
                    {
                        return string.Empty;
                    }
                }
            }
        }


با استفاده از پارامتر: (بهتر است نوع دیتا تایپ پارامتر و طول آن ذکر شود)

[HttpGet]
        [Route("nonsensitivewithparam")]
        public string GetNonSensitiveDataByNameWithParam()
        {
            using (SqlConnection connection = new SqlConnection(_configuration.GetValue<string>("ConnectionString")))
            {
                connection.Open();
                SqlCommand command = new SqlCommand($"SELECT * FROM NonSensitiveDataTable WHERE Name = @name", connection);
                command.Parameters.AddWithValue("@name", Request.Query["name"].ToString());
                using (var reader = command.ExecuteReader())
                {
                    if (reader.Read())
                    {
                        string returnString = string.Empty;
                        returnString += $"Name : {reader["Name"]}. ";
                        returnString += $"Description : {reader["Description"]}";
                        return returnString;
                    }
                    else
                    {
                        return string.Empty;
                    }
                }
            }
        }


3) از Stored Procedureها استفاده کنید و بصورت پارامتری داده‌های مورد نیاز را به آن‌ها پاس دهید: (بهتر است نوع دیتا تایپ پارامتر و طول آن ذکر شود)  

 [HttpGet]
        [Route("nonsensitivewithsp")]
        public string GetNonSensitiveDataByNameWithSP()
        {
            using (SqlConnection connection = new SqlConnection(_configuration.GetValue<string>("ConnectionString")))
            {
                connection.Open();
                SqlCommand command = new SqlCommand("SP_GetNonSensitiveDataByName", connection);
                command.CommandType = System.Data.CommandType.StoredProcedure;
                command.Parameters.AddWithValue("@name", Request.Query["name"].ToString());
                using (var reader = command.ExecuteReader())
                {
                    if (reader.Read())
                    {
                        string returnString = string.Empty;
                        returnString += $"Name : {reader["Name"]}. ";
                        returnString += $"Description : {reader["Description"]}";
                        return returnString;
                    }
                    else
                    {
                        return string.Empty;
                    }
                }
            }
        }


4) اگر از داینامیک کوئری استفاده میکنید، داده‌های مورد استفاده‌ی در کوئری را بصورت پارامتری ارسال کنید:

فرض کنید چنین جدولی دارید

CREATE TABLE tbl_Product
(
Name NVARCHAR(50),
Qty INT,
Price FLOAT
)

GO

INSERT INTO tbl_Product (Name, Qty, Price) VALUES (N'Shampoo', 200, 10.0);
INSERT INTO tbl_Product (Name, Qty, Price) VALUES (N'Hair Clay', 400, 20.0);
INSERT INTO tbl_Product (Name, Qty, Price) VALUES (N'Hair Tonic', 300, 30.0);

یک پروسیجر را دارید که عملیات جستجو را انجام میدهد و از داینامیک کوئری استفاده میکند.

ALTER PROCEDURE sp_GetProduct(@Name NVARCHAR(50))
AS
BEGIN
    DECLARE @sqlcmd NVARCHAR(MAX);
    SET @sqlcmd = N'SELECT * FROM tbl_Product WHERE Name = ''' + @Name + '''';
    EXECUTE(@sqlcmd)
END

با اینکه از Stored Procedure استفاده میکنید، باز هم در معرض خطر SQLi می‌باشید. فرض کنید هکر چنین درخواستی را ارسال میکند:

Shampoo'; DROP TABLE tbl_Product; --

نتیجه، تبدیل به دستور زیر میشود:

SELECT * FROM tbl_Product WHERE Name = 'Shampoo'; DROP TABLE tbl_Product; --'

برای جلوگیری از SQLi در کوئریهای داینامیک SP بشکل زیر عمل میکنیم:

ALTER PROCEDURE sp_GetProduct(@Name NVARCHAR(50))
AS
BEGIN
    DECLARE @sqlcmd NVARCHAR(MAX);
    DECLARE @params NVARCHAR(MAX);
    SET @sqlcmd = N'SELECT * FROM tbl_Product WHERE Name = @Name';
    SET @params = N'@Name NVARCHAR(50)';
    EXECUTE sp_executesql @sqlcmd, @params, @Name;
END
در اینجا در پارامتر params@ نوع دیتاتایپ و طول آن را مشخص میکنید و کارکتر quote به صورت خودکار حذف میشود و از SQLi جلوگیری میکند. 


5) میتوان از تنظیمات IIS یا وب سرورهای دیگر برای جلوگیری از SQLi استفاده نمود.

6) استفاده از چند کاربرِ دیتابیس در برنامه و بکارگیری سطح دسترسی محدود و مناسب( ^ , ^ ).

7) از  ORM استفاده کنید و اگر نیاز به سرعت بیشتری دارید از یک Micro ORM استفاده کنید؛ با در نظر داشتن نکات لازم  

نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی
با عرض سلام طبق مستندات Identity Server ، ثبت Claims‌ها در تابع  GetProfileDataAsync  بهتر است به صورت زیر نوشته شود:
  public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            var claimsForUser = await _usersService.GetUserClaimsBySubjectIdAsync(subjectId).Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList();
             context.AddRequestedClaims(claimsForUser );
        }    
زیرا تابع AddRequestedClaims  تنها Claims هایی که  کلاینت درخواست کرده را باز میگرداند ولی در صورتی که Claims ‌ها به طور مستقیم به IssuedClaims   اضافه شود حتی Claims  هایی که کلاینت درخواست نکرده نیز برگردانده میشود. این مشکل میتواند یک نقص امنیتی به وجود بیاورد که کلاینت از طریق آن میتواند به Claims هایی که به آنها دسترسی ندارد و آنها را درخواست نکرده است دسترسی داشته باشد!
مطالب
رمزنگاری کانکشن استرینگ در ASP.Net

ذخیره کردن رشته اتصالی به دیتابیس، به صورت یک رشته مشخص در کدهای برنامه، کاری است مزموم. زیرا پس از هر بار تغییر این مورد، نیاز خواهد بود تا تمامی سورس‌ها تغییر کنند و اگر از حالت web application استفاده کرده باشید، مجبور خواهید شد یکبار دیگر برنامه را کامپایل و دایرکتوری bin روی سرور را به روز کنید. به همین جهت، استاندارد برنامه‌های ASP.Net این است که این رشته اتصالی را در فایل web.config ذخیره کنیم تا با هر بار تغییر پارامترهای مختلف آن (مثلا تغییر نام سرور، یا تعویض ماهیانه پسوردها)، مجبور به کامپایل مجدد برنامه نشویم. شبیه به همین مورد در برنامه‌های PHP هم رایج است و عموما این مشخصات در فایل config.php و یا با اسامی شبیه به این صورت می‌گیرد.
در ASP.Net 1.x قسمت خاصی برای کانکشن استرینگ وجود نداشت اما از ASP.Net 2 به بعد ، قسمت ویژه‌ای مخصوص این کار در فایل web.config در نظر گرفته شده است.
خیلی هم خوب! اما این تجربه تلخ کاری را (که یکبار برای من رخ داد) هم همواره در نظر داشته باشید:
امکان خوانده شدن محتوای فایل کانفیگ، توسط همسایه شما در همان هاست اشتراکی که الان از آن دارید استفاده می‌کنید. عموما هاست‌های اینترنتی اشتراکی هستند و نه dedicated و نه فقط مختص به شما. از یک سرور برای سرویس دهی به 100 ها سایت استفاده می‌شود. یکبار در یکی از سایت‌ها دیدم که فایل machine.config سرور را هم محض نمونه خوانده بودند چه برسد به فایل متنی کانفیگ شما! یا تصور کنید که وب سرور هک شود. عموما اس کیوال سرور بر روی سرور دیگری قرار دارد. به همین جهت رمزنگاری این رشته باز هم ضریب امنیت بیشتری را به همراه خواهد داشت.
به همین منظور رمزنگاری قسمت کانکشن استرینگ فایل وب کانفیگ الزامی است، چون آن‌هایی که به دنبال اطلاعاتی اینگونه هستند دقیقا می‌دانند باید به کجا مراجعه کنند.

راه حل‌ها:

الف) از وب کانفیگ برای این‌کار استفاده نکنید. یک فایل class library‌ درست کنید (یک dll مجزا) و ارجاعی از این فایل را به پروژه خود اضافه کنید و از رشته اتصالی قرار گرفته در آن استفاده کنید. این فایل را هم می‌توان با روش‌های obfuscation محافظت کرد تا امنیت اطلاعات داخل آن‌را تا حد قابل قبولی بالا برد. همچنین می‌توان برای این فایل کتابخانه، امضای دیجیتال درنظر گرفت. زیرا امضای دیجیتال سبب می‌شود تا تغییر فایل dll رشته اتصالی، با یک کپی و paste معمولی قابل انجام نباشد (تمامی dll ها و اسمبلی‌های دیگری که ارجاعی از آن‌را در خود دارند باید یکبار دیگر هم کامپایل و به سرور منتقل شوند). این یک نوع اطمینان خاطر است اما در بلند مدت شاید تکرار اینکار خسته کننده باشد.

ب)استفاده از روش استاندارد رمزنگاری قسمت‌های مختلف کانکشن استرینگ فایل web.config
برای مشاهده نحوه انجام اینکار با برنامه نویسی به این مقاله مراجعه نمائید.
مزیت: نیازی به کد نویسی برای رمزگشایی و استفاده از آن نیست و اینکار به صورت خودکار توسط ASP.Net انجام می‌شود.
ایراد:فایل حاصل قابل انتقال نیست. چون رمزنگاری بر اساس کلیدهای منحصربفرد سرور شما ایجاد می‌شوند، این فایل از یک سرور به سرور دیگر قابل انتقال و استفاده نخواهد بود. یعنی اگر بر روی کامپیوتر برنامه نویسی شما این‌کار صورت گرفت، برنامه در سرور کار نخواهد کرد. البته شاید ایراد آنچنانی نباشد و فقط باید یکبار دیگر روی هاست نیز این کار را تکرار کرد. اما باید درنظر داشت که همسایه محترم شما نیز می‌تواند بر روی همان هاست به سادگی فایل شما را رمزگشایی کند! بنابراین نباید اصلا به این روش در هاست‌های اشتراکی دل خوش کرد.

ج)بکارگیری روش‌های غیراستاندارد رمزنگاری
منظور از غیراستاندارد، حالت‌های دیگر استاندارد رمزنگاری و رمزگشایی نسبت به روش استاندارد ارائه شده توسط مایکروسافت است (که همه از آن مطلع هستند). به شخصه از این روش در هاست‌ها استفاده می‌کنم. (مثلا، البته با کمی تغییر و پیچ و تاب بیشتر)
الگوریتم‌های رمزنگاری و رمزگشایی در یک فایل dll به برنامه اضافه می‌شوند (بنابراین این فایل قرار نیست تغییر کند). رشته رمزنگاری شده در فایل web.config قرار می‌گیرد. بدیهی است در هر بار اتصال به دیتابیس این رشته باید رمزگشایی شود اما سربار آن بسیار کم است و اصلا مشهود نیست. در هر حال این هزینه‌ای است که باید پرداخت شود. بدست آوردن ساده کانکشن استرینگ یعنی امکان پاک کردن سریع کل اطلاعات شما.

د)اگر سرور dedicated است حتما از روش windows authentication استفاده کنید
برای مثال یک سرور dedicated مخصوص کار ویژه‌ای تهیه کرده اید یا در شبکه اینترانت یک شرکت برنامه شما نصب شده است.
روش اعتبار سنجی از نوع ویندوزی برای اتصال به اس کیوال سرور نسبت به حالت sql server authentication امن تر است، زیرا نیازی نیست تا در وب کانفیگ نام کاربری یا پسوردی را مشخص نمائید و همچنین در این حالت پسوردها در شبکه منتقل نمی‌شوند (در حالت sql server authentication اینطور نیست). اما عموما در هاست‌های اشتراکی برای ساده تر کردن کار ، از این روش استفاده نمی‌کنند.
بنابراین در اینجا حتی اگر شخصی به رشته اتصالی شما دسترسی پیدا کند، کار خاصی را نمی‌تواند انجام دهد چون هیچگونه نام کاربری یا پسوردی در آن لحاظ نشده است.
در این روش به صورت پیش فرض از اکانت ASP.Net استفاده می‌شود. یعنی تمام برنامه‌ها محدود به یک اکانت خواهند شد.
برای تغییر این مورد دو کار را می‌توان انجام داد : استفاده از impersonation یا مطالعه قسمت بعد (ه)
توصیه: از روش impersonation به دلیل اینکه باید نام کاربری و کلمه عبور را باز هم به صورت واضحی ذکر نمود اجتناب کنید.

ه)ایجاد application pool مجزا به ازای هر برنامه ASP.Net در ویندوزهای سرور
Application pool که برای اولین بار در ویندوز سرور 2003 معرفی شده جهت ایزوله کردن برنامه‌های ASP.Net بکار برده می‌شود. به این صورت می‌شود برای هر pool یک اکانت ویندوزی مجزا تعریف کرد. حال می‌توان به این اکانت در اس کیوال سرور دسترسی داد. به این صورت برنامه‌های مختلف تحت یک اکانت واحد (یوزر asp.net) کار نکرده (می‌توانند هم کار کنند، اما امکان تعریف identity جدید برای کاربر آن در IIS‌ وجود دارد) و ضریب امنیتی بالاتری را تجربه خواهید کرد (در تکمیل روش (د))


مطالب
مدیریت طول عمر DbContext در برنامه‌های Blazor SSR

فرض کنید یک صفحه‌ی Blazor SSR، از سه کامپوننت منوی سمت راست، محتوای اصلی صفحه و فوتر سایت که به همراه متنی است، تشکیل شده‌است. منوی سمت راست، به همراه لینک‌هایی‌است که آمار آن‌ها را نیز نمایش می‌دهد و این اطلاعات را از بانک اطلاعاتی، به کمک EF-Core دریافت می‌کند. فوتر صفحه، سال شروع به کار و نام برنامه را از بانک اطلاعاتی دریافت می‌کند و محتوای اصلی صفحه نیز از بانک اطلاعاتی دریافت می‌شود. پس از تکمیل این سه کامپوننت مجزا، اگر برنامه را اجرا کنید، بلافاصله با خطای زیر مواجه می‌شوید:

A second operation started on this context before a previous operation completed

مشکل کجاست؟! مشکل اینجاست که تنها یک نمونه از DbContext، در طول درخواست جاری رسیده، بین سه کامپوننت جاری برنامه به اشتراک گذاشته می‌شود (به سازنده‌ی سرویس‌های مرتبط تزریق می‌شود) و ... در Blazor SSR، پردازش کامپوننت‌های یک صفحه، به صورت موازی و همزمان انجام می‌شوند؛ یعنی ترتیبی نیست. اگر ابتدا کامپوننت منو، بعد محتوای صفحه و در آخر فوتر، رندر می‌شدند، هیچگاه پیام فوق را مشاهده نمی‌کردیم؛ اما ... هر سه کامپوننت، با هم و همزمان رندر می‌شوند و سپس نتیجه‌ی نهایی در Response درج خواهد شد. یعنی یک DbContext بین چندین ترد به اشتراک گذاشته می‌شود که چنین حالتی توسط EF-Core پشتیبانی نمی‌شود و مجاز نیست.

روش مواجه شدن با یک چنین حالت‌هایی، نمونه سازی مجزای DbContext به ازای هر کامپوننت است که نمونه‌ای از آن‌را پیشتر در مطلب «نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server» مشاهده کرده‌اید. در این مطلب، راه‌حل دیگری برای اینکار ارائه می‌شود که ساده‌تر است و نیازی به تغییرات آنچنانی در کدهای کامپوننت‌ها و کل برنامه ندارد.

استفاده از کلاس پایه‌ی OwningComponentBase برای نمونه سازی مجدد DbContext به‌ازای هر کامپوننت

زمانیکه در برنامه‌های Blazor SSR از روش استاندارد زیر برای دسترسی به سرویس‌های مختلف برنامه استفاده می‌کنیم:

@inject IHotelRoomService HotelRoomService

طول عمر دریافتی سرویس، دقیقا بر اساس طول عمر اصلی تعریف شده‌ی آن عمل می‌کند (شبیه به برنامه‌های ASP.NET Core). یعنی برای مثال اگر Scoped باشد، DbContext تزریق شده‌ی در آن هم Scoped است و این DbContext، بین تمام کامپوننت‌های در حال پردازش موازی در طول یک درخواست، به‌اشتراک گذاشته می‌شود که مطلوب ما نیست. ما می‌خواهیم بتوانیم به ازای هر کامپوننت مجزای صفحه، یک DbContext جدید داشته باشیم. یعنی باید بتوانیم خودمان این سرویس Scoped را نمونه سازی کنیم و نه اینکه آن‌را مستقیما از سیستم تزریق وابستگی‌ها دریافت کنیم.

بنابراین اگر بخواهیم قسمت‌های مختلف برنامه را تغییر ندهیم و همان تعاریف ابتدایی services.AddDbContext و Scoped تعریف کردن سرویس‌های برنامه بدون تغییر باقی بمانند (و از IDbContextFactory و موارد مشابه دیگر مطلب «نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server» هم استفاده نکنیم)، باید جایگزینی را برای نمونه سازی سرویس‌ها ارائه دهیم. به همین جهت در ابتدا، یک ویژگی جدیدی را به صورت زیر تعریف می‌کنیم:

[AttributeUsage(AttributeTargets.Property)]
public sealed class InjectComponentScopedAttribute : Attribute
{
}

تا بتوانیم بجای:

@inject IHotelRoomService HotelRoomService

بنویسیم:

[InjectComponentScoped] internal IHotelRoomService HotelRoomService { set; get; } = null!;

مرحله‌ی بعد، نوبت به نمونه سازی خودکار این سرویس‌های درخواستی علامتگذاری شده‌ی با InjectComponentScoped است. برای این منظور، تمام کامپوننت‌های برنامه را از کلاس پایه و استاندارد OwningComponentBase ارث‌بری می‌کنیم. مزیت اینکار، امکان دسترسی به خاصیتی به نام ScopedServices در تمام کامپوننت‌های برنامه است که توسط آن می‌توان به متد ScopedServices.GetRequiredService آن دسترسی یافت. یعنی با ارث‌بری از کلاس پایه‌ی OwningComponentBase به ازای هر کامپوننت، به صورت خودکار Scope جدیدی شروع می‌شود که توسط آن می‌توان به نمونه‌ی جدیدی از سرویس مدنظر دسترسی یافت و نه به نمونه‌ی اشتراکی در طی درخواست جاری.

اکنون اگر از این مزیت به صورت زیر استفاده کنیم، می‌توان تمام سرویس‌های درخواستی مزین به InjectComponentScopedAttribute یک کامپوننت را به صورت خودکار یافته و با استفاده از ScopedServices.GetRequiredService، مقدار دهی کرد:

public class BlazorScopedComponentBase : OwningComponentBase
{
    private static readonly ConcurrentDictionary<Type, Lazy<List<PropertyInfo>>> CachedProperties = new();

    private List<PropertyInfo> InjectComponentScopedPropertiesList => CachedProperties.GetOrAdd(GetType(),
            type => new Lazy<List<PropertyInfo>>(
                () => type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance |
                                         BindingFlags.NonPublic | BindingFlags.Public)
                    .Where(p => p.GetCustomAttribute<InjectComponentScopedAttribute>() is not null)
                    .ToList(), LazyThreadSafetyMode.ExecutionAndPublication)).Value;

    protected override void OnInitialized()
    {
        foreach (var propertyInfo in InjectComponentScopedPropertiesList)
        {
            propertyInfo.SetValue(this, ScopedServices.GetRequiredService(propertyInfo.PropertyType));
        }
    }
}

این سرویس، اینبار طول عمری، محدود به کامپوننت جاری را خواهد داشت و بین سایر کامپوننت‌های درحال پردازش درخواست جاری، به اشتراک گذاشته نمی‌شود و همچنین به صورت خودکار هم در پایان درخواست، Dispose می‌شود.

فعالسازی ارث‌بری خودکار در تمام کامپوننت‌های برنامه

مرحله‌ی بعد، ارث‌بری خودکار تمام کامپوننت‌های برنامه از OwningComponentBase سفارشی فوق است و در اینجا قصد نداریم تمام کامپوننت‌ها را جهت معرفی آن، به صورت دستی تغییر دهیم. برای اینکار فقط کافی است به فایل Imports.razor_ مراجعه و یک سطر زیر را در آن درج کنیم:

@inherits BlazorScopedComponentBase

با اینکار یک ارث‌بری سراسری در کل برنامه رخ می‌دهد و تمام کامپوننت‌ها، از BlazorScopedComponentBase مشتق خواهند شد. یعنی پس از این تغییر، اگر سرویسی را به صورت زیر معرفی و با ویژگی InjectComponentScoped علامتگذاری کردیم:

[InjectComponentScoped] internal IHotelRoomService HotelRoomService { set; get; } = null!;

به صورت خودکار یافت شده و نمونه سازی Scoped محدود به طول عمر همان کامپوننت می‌شود که بین سایر کامپوننت‌ها، به اشتراک گذاشته نخواهد شد.

یک نکته: اگر کامپوننت شما متد OnInitialized را بازنویسی می‌کند، ‌فراموش نکنید که در ابتدای آن باید ()base.OnInitialized را هم فراخوانی کنید تا متد OnInitialized کامپوننت پایه‌ی BlazorScopedComponentBase نیز فراخوانی شود. البته این مورد در حین بازنویسی نمونه‌ی async آن مهم نیست؛ چون همیشه OnInitialized غیر async در ابتدا فراخوانی می‌شود و سپس نمونه‌ی async آن اجرا خواهد شد.

مطالب
انتقال فایل‌های دیتابیس اس کیوال سرور 2008

روز قبل نیاز بود تا فایل‌های mdf و ldf دیتابیس‌ها جابجا شوند (یک هارد بزرگتر و از این مسایل).
برای جابجا کردن این فایل‌ها هم روش معمول detach و سپس attach است. ابتدا روی دیتابیس کلیک راست کرده و detach . حالا فایل‌ها را جابجا می‌کنید و سپس attach . یا می‌شود بک آپ کامل گرفت و بعد ری استور کرد.
عموما هم نمی‌توان دیتابیس در حال استفاده را detach‌ کرد. باید دیتابیس ابتدا single user شود و بعد می‌توان این‌کار را انجام داد.
تا اینجای کار متداول است. همه چیز به خوبی انجام شد. سپس در لحظه attach ، دیتابیس‌ها به صورت read only اتچ شدند با آیکونی سیاه رنگ در management studio . (و رنگ من هم بلافاصله به همین رنگ متمایل شد!)
بعد از مدتی جستجو مشخص شد که در اس کیوال سرور 2008 برای کاهش سطح حمله به سرور، از یک سری یوزر با دسترسی کم برای نصب اس کیوال سرور استفاده می‌شود (بسیار هم خوب) و اس کیوال سرور 2008 ، یک سری یوزر مخصوص را هم در حین نصب ایجاد می‌کند که به صورت خودکار بر روی پوشه دیتای شما دسترسی full control دارد برای اینکه بتواند کارش را انجام دهد.
حال اگر شما در جای دیگری پوشه‌ای درست کردید و این دیتابیس‌ها را منتقل نمودید، مجددا پیش از هر کاری باید این دسترسی را برقرار کنید و گرنه اس کیوال سرور مجوز write نخواهد داشت؛ به همین جهت دیتابیس به صورت read only در management studio با رنگ مشکی ظاهر می‌شود.
نام این کاربر مخصوص به صورت زیر است:

SQLServerMSSQLUser$ComputerName$MSSQLSERVER




پس از برقراری دسترسی هم مشکل برطرف نمی‌شود. باید دستور زیر را نیز اجرا نمود:
ALTER DATABASE myDB SET READ_WRITE
اجرای این دستور نیز، نیاز به حالت single user دارد.

پ.ن.
می‌توان دسترسی یوزر سرویس اس کیوال سرور 2008 را نیز مانند نگارش‌های قبلی به حالت local system تغییر داد (یا هر اکانت دیگری با دسترسی بالا) تا این مشکلات نباشد؛ ولی بدیهی است سطح حمله به سرور نیز به همین اندازه افزایش می‌یابد.

مطالب
فقط به خاطر یک نیم فاصله!
نسخه‌ی 64 بیتی ویندوز 7 را نصب کرده‌ام و اولین مشکل، نبود صفحه کلید مطابق استاندارد 2091 برای نسخه‌های 64 بیتی ویندوز است. پروژه وب فارسی دانشگاه صنعتی شریف، سال‌ها قبل فقط یک نسخه‌ی 32 بیتی از آن را تهیه کرده و نسخه‌های 64 بیتی موجود، مطابق استاندارد 9147 هستند و من به دلایل ذیل حاضر به استفاده از آن نیستم!
  1. در استاندارد 9147 ، جای ژ و پ مطابق صفحه کلیدی که در بازار ایران فروخته می‌شود نیست (و باید به همه پاسخگو باشید که چرا اینطوری است).
  2. ی و ک در آن اصلاح شده و دیگر عربی نیست که چقدر هم خوب، اما من تعداد زیادی برنامه و همچنین تعداد قابل توجهی فایل word دارم که مطابق استاندارد 2091 تهیه شده‌اند. مشکلات ی و ک فارسی و عربی را هم در اینجا ذکر کرده‌ام و به دنبال باگ‌های جدید در برنامه‌ها نمی‌گردم.
  3. با ی و ک فارسی اگر در گوگل جستجو کنید تعداد پاسخ‌های مرتبط یافت شده بسیار بسیار کمتر از حالتی خواهد بود که با ی و ک عربی جستجو کنید.
  4. در یک سازمان باید رویه‌ا‌ی واحد در این مورد برقرار باشد. یا همه باید از 2091 استفاده کنند و یا همه از 9147.

نسخه‌ی کامل استاندارد 9147 را از سایت آقای اخگری می‌توانید دریافت نمائید:

به همین دلایل خصوصا قسمت دوم (هر چند ممکن است با آن موافق نباشید)، نیاز به صفحه کلید مطابق استاندارد 2091 نسخه‌ی 64 بیتی داشتم.

برای تهیه صفحه کلید فارسی از برنامه Microsoft Keyboard Layout Creator استفاده می‌شود که نسخه‌ی 1.4 آن‌را از آدرس زیر می‌توانید دریافت نمائید. این نسخه قابلیت تولید فایل‌های dll مرتبط 64 بیتی را هم دارد:


یکی از قابلیت‌های این برنامه بارگذاری صفحه کلید فارسی جاری سیستم است:



صفحه کلید استاندارد 2091 پروژه وب فارسی را اگر با آن باز کرده و سپس از منوی Project گزینه‌ی Build DLL and setup package را انتخاب نمائید، با خطای زیر متوقف خواهید شد:
ERROR: 'VK_SPACE' in Shift State 'Shift' must be made up of white space character(s), but is defined as '‌' (U+200c) instead.
که دقیقا مربوطه به این سطر در تعاریف صفحه کلید است (صفحه کلید را می‌شود به صورت فایلی با پسوند klc هم save as کرد):
39 SPACE  0 0020 200c 0020 -1  // SPACE, ZERO WIDTH NON-JOINER, SPACE, <none>
مفاهیم این ستون‌ها هم به شرح زیر هستند:
0 //Column 4
1 //Column 5 : Shft
2 //Column 6 :       Ctrl
3 //Column 7 : Shft  Ctrl
یعنی جهت نمایش نیم فاصله حاصل از ترکیب shift space مطابق استاندارد تایپ ایران، بجای 0x0020 یا همان فاصله، از 0x200C استفاده شده است (مطابق استاندارد تایپ ایران باید نوشت "می‌روم" و نه "می روم". به نیم فاصله و فاصله دقت بفرمائید).
اما این برنامه‌ی محترم دقیقا همین مورد را غلط گرفته و فایل dll نهایی را تولید نمی‌کند (مطابق خطایی که ذکر شد).
برنامه‌ی Microsoft Keyboard Layout Creator هم با دات نت فریم ورک نوشته شده است. اگر کمی با reflector به آنالیز آن بپردازیم به کلاس Accept و متد زیر خواهیم رسید:
private bool VerifySpaceBarIntegrity(ShiftState ss, bool fMustBeDefined)
در این متد جایی که خطای ذکر شده صادر می‌شود، مربوط به چند سطر زیر است:
if (!Utilities.IsSpacing(ss.Characters))
{
    this.WriteErrorToLogFile("SpaceKeyNeedsWhiteSpaceCharacters", new string[] { "VK_" + Utilities.VkStringOfIvk(ss.VK), this.m_stStateLabel[ss.State], ss.Characters, Utilities.FromCharacterToUPlusForm(ss.Characters) });
    flag = false; /*اینجا باید اصلاح شود!*/
}
خوب! به نظر شما ایرادی دارد که این flag همیشه true باشد؟! برای همین منظور، فایل MSKLC.exe را پچ کرده‌ام که از آدرس زیر قابل دریافت است:

  دریافت پچ شده نگارش 1.4.6000.2 : MSKLC.Patched.7z  

با استفاده از این نسخه (ابتدا برنامه اصلی را نصب کرده و سپس فایل exe را جایگزین نمائید)، به راحتی می‌توان نسخه‌ی استاندارد و اصلی 2091 را بارگذاری و مجددا کامپایل کرد (بدون توقف کامپایل به خاطر فقط یک نیم فاصله‌ی غیرمعتبر از دیدگاه نویسندگان اصلی برنامه). به این صورت فایل‌های 64 بیتی لازم هم تولید می‌شوند ( ممکن است در حین کار با برنامه‌ی patch شده، نمایش خطای کار با نیم فاصله را مجددا دریافت کنید؛ اما مهم نیست . نمونه‌ی وصله شده، بدون مشکل حاصل را کامپایل می‌کند که در نمونه‌ی اصلی اینطور نیست. یعنی پس از overwrite کردن فایل exe موجود، با نمونه patch شده، برنامه کار کامپایل را کامل می‌کند) که حاصل نهایی را از آدرس زیر می‌توانید دریافت نمائید:


این فایل‌ها دقیقا بر مبنای همان فایل‌های پروژه وب فارسی استاندارد 2091 هستند؛ با این تفاوت که نسخه‌‌های غیر 32 بیتی هم در آن لحاظ شده‌اند.

نصب این dll‌ها هم از ویندوز ویستا به بعد داستان خودش را پیدا کرده؛ ابتدا باید take ownership شود، سپس می‌توان فایل اصلی را به سادگی جایگزین کرد و سپس reboot .
برای این منظور ابتدا به پوشه‌ی system32 ویندوز مراجعه کرده و فایل KBDFA.DLL را پیدا کنید.
در ادامه به پنجره خواص آن (کلیک راست و انتخاب properties) مراجعه و برگه‌ی Security آن‌را انتخاب کنید.
در این قسمت بر روی دکمه‌ی Advanced کلیک نمائید.
در صفحه‌ی باز شده به برگه‌ی Owner مراجعه کنید.
در این قسمت بر روی دکمه‌ی Edit کلیک نموده و کاربر خودتان را اضافه نمائید.
پس از طی این مرحله (یا همان take ownership) به برگه security مراجعه کرده و به کاربر خودتان دسترسی full control بدهید.
اکنون مجوز تغییر این فایل را بدون هیچ مشکلی خواهید یافت.
در پایان reboot را فراموش نکنید.

راه دوم:
یا برنامه نصاب حاصل از برنامه‌ی Microsoft Keyboard Layout Creator، مدخل جدیدی را به صفحه کلیدهای قابل انتخاب ویندوز در کنترل پنل اضافه می‌کند که نیازی به reboot ندارد.
مطالب
ایجاد جداول بهینه سازی شده برای حافظه در SQL Server 2014
پس از نگاهی به مفاهیم مقدماتی OLTP درون حافظه‌ای در SQL Server 2014، در ادامه به نحوه‌ی انجام تنظیمات خاص جداول بهینه سازی شده برای حافظه خواهیم پرداخت.


ایجاد یک بانک اطلاعاتی با پشتیبانی از جداول بهینه سازی شده برای حافظه

برای ایجاد جداول بهینه سازی شده برای حافظه، ابتدا نیاز است تا تنظیمات خاصی را به بانک اطلاعاتی آن اعمال کنیم. برای اینکار می‌توان یک بانک اطلاعاتی جدید را به همراه یک filestream filegroup ایجاد کرد که جهت جداول بهینه سازی شده برای حافظه، ضروری است؛ یا اینکه با تغییر یک بانک اطلاعاتی موجود و افزودن filegroup یاد شده نیز می‌توان به این مقصود رسید.
در اینگونه جداول خاص، اطلاعات در حافظه‌ی سیستم ذخیره می‌شوند و برخلاف جداول مبتنی بر دیسک سخت، صفحات اطلاعات وجود نداشته و نیازی نیست تا به کش بافر وارد شوند. برای مقاصد ذخیره سازی نهایی اطلاعات جداول بهینه سازی شده برای حافظه، موتور OLTP درون حافظه‌ای آن، فایل‌های خاصی را به نام checkpoint در یک filestream filegroup ایجاد می‌کند که از آن‌ها جهت ردیابی اطلاعات استفاده خواهد کرد و نحوی ذخیره سازی اطلاعات در آن‌ها از شیوه‌ی با کارآیی بالایی به نام append only mode پیروی می‌کند.
با توجه به متفاوت بودن نحوه‌ی ذخیره سازی نهایی اطلاعات اینگونه جداول و دسترسی به آن‌ها از طریق استریم‌ها، توصیه شده‌است که filestream filegroup‌های تهیه شده را در یک SSD یا Solid State Drive قرار دهید.

پس از اینکه بانک اطلاعاتی خود را به روش‌های معمول ایجاد کردید، به برگه‌ی خواص آن در management studio مراجعه کنید. سپس صفحه‌ی file groups را در آن انتخاب کرده و در پایین برگه‌ی آن، در قسمت جدید memory optimized data، بر روی دکمه‌ی Add کلیک کنید. سپس نام دلخواهی را وارد نمائید.


پس از ایجاد یک گروه فایل جدید، به صفحه‌ی files خواص بانک اطلاعاتی مراجعه کرده و بر روی دکمه‌ی Add کلیک کنید. سپس File type این ردیف اضافه شده را از نوع file stream data و file group آن‌را همان گروه فایلی که پیشتر ایجاد کردیم، تنظیم کنید. در ادامه logical name دلخواهی را وارد کرده و در آخر بر روی دکمه‌ی Ok کلیک کنید تا تنظیمات مورد نیاز جهت تعاریف جدول بهینه سازی شده برای حافظه به پایان برسد.


این مراحل را توسط دو دستور T-SQL ذیل نیز می‌توان سریعتر انجام داد:
USE [master]
GO
ALTER DATABASE [testdb2] 
      ADD FILEGROUP [InMemory_InMemory] CONTAINS MEMORY_OPTIMIZED_DATA 
GO
ALTER DATABASE [testdb2] 
      ADD FILE ( NAME = N'InMemory_InMemory', FILENAME = N'D:\SQL_Data\MSSQL11.MSSQLSERVER\MSSQL\DATA\InMemory_InMemory' ) 
      TO FILEGROUP [InMemory_InMemory]
GO

ساختار گروه فایل بهینه سازی شده برای حافظه

گروه فایل بهینه سازی شده برای حافظه، دارای چندین دربرگیرنده است که هر کدام چندین فایل را در خود جای خواهند داد:
- Root File که در برگیرنده‌ی متادیتای اطلاعات است.
- Data File که شامل ردیف‌های اطلاعات ثبت شده در جداول بهینه سازی شده‌ی برای حافظه هستند. این ردیف‌ها همواره به انتهای data file اضافه می‌شوند و دسترسی به آن‌ها ترتیبی است. کارآیی IO این روش نسبت به روش دسترسی اتفاقی به مراتب بالاتر است. حداکثر اندازه این فایل 128 مگابایت است و پس از آن یک فایل جدید ساخته می‌شود.
- Delta File شامل ردیف‌هایی است که حذف شده‌اند. به ازای هر ردیف، حداقل اطلاعاتی از آن را در خود ذخیره خواهد کرد؛ شامل ID ردیف حذف شده و شماره تراکنش آن. همانطور که پیشتر نیز ذکر شد، این موتور جدید درون حافظه‌ای، برای یافتن راه چاره‌ای جهت به حداقل رسانی قفل گذاری بر روی اطلاعات، چندین نگارش از ردیف‌ها را به همراه timestamp آن‌ها در خود ذخیره می‌کند. به این ترتیب، هر به روز رسانی به همراه یک حذف و سپس ثبت جدید است. به این ترتیب دیگر بانک اطلاعاتی نیازی نخواهد داشت تا به دنبال رکورد موجود برگردد و سپس اطلاعات آن‌را به روز نماید. این موتور جدید فقط اطلاعات به روز شده را در انتهای رکوردهای موجود با فرمت خود ثبت می‌کند.


ایجاد جداول بهینه سازی شده برای حافظه

پس از آماده سازی بانک اطلاعاتی خود و افزودن گروه فایل استریم جدیدی به آن برای ذخیره سازی اطلاعات جداول بهینه سازی شده برای حافظه، اکنون می‌توانیم اینگونه جداول خاص را در کنار سایر جداول متداول موجود، تعریف و استفاده نمائیم:
-- It is not a Memory Optimized
CREATE TABLE tblNormal
(
   [CustomerID] int NOT NULL PRIMARY KEY NONCLUSTERED, 
   [Name] nvarchar(250) NOT NULL,
   CustomerSince DATETIME not NULL
      INDEX [ICustomerSince] NONCLUSTERED
)

--  DURABILITY = SCHEMA_AND_DATA
CREATE TABLE tblMemoryOptimized_Schema_And_Data
(
    [CustomerID] INT NOT NULL 
PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000),
    [Name] NVARCHAR(250) NOT NULL,
    [CustomerSince] DATETIME NOT NULL
INDEX [ICustomerSince] NONCLUSTERED
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA)


-- DURABILITY = SCHEMA_ONLY
CREATE TABLE tblMemoryOptimized_Schema_Only
(
    [CustomerID] INT NOT NULL 
PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000),
    [Name] NVARCHAR(250) NOT NULL,
    [CustomerSince] DATETIME NOT NULL
INDEX [ICustomerSince] NONCLUSTERED
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY)
در اینجا سه جدول را مشاهده می‌کنید که در بانک اطلاعاتی آماده شده در مرحله‌ی قبل، ایجاد خواهند شد. مورد اول یک جدول معمولی است که از آن برای مقایسه سرعت ثبت اطلاعات با سایر جداول ایجاد شده، استفاده خواهد شد.
همانطور که مشخص است، دو جدول بهینه سازی شده برای حافظه، همان سه ستون جدول معمولی مبتنی بر دیسک سخت را دارا هستند؛ اما با این تفاوت‌ها:
- دارای ویژگی MEMORY_OPTIMIZED = ON می‌باشند. به این ترتیب اینگونه جداول نسبت به جداول متداول مبتنی به دیسک سخت متمایز خواهند شد.
- دارای ویژگی DURABILITY بوده و توسط مقدار SCHEMA_AND_DATA آن مشخص می‌کنیم که آیا قرار است اطلاعات و ساختار جدول، ذخیره شوند یا تنها قرار است ساختار جدول ذخیره گردد (حالت SCHEMA_ONLY).
- بر روی ستون Id آن‌ها یک hash index ایجاد شده‌است که وجود آن ضروری است و در کل بیش از 8 ایندکس را نمی‌توان تعریف کرد.
برخلاف ایندکس‌های B-tree جداول مبتنی بر سخت دیسک، ایندکس‌های جداول بهینه سازی شده برای حافظه، اطلاعات را تکرار نمی‌کنند. این‌ها صرفا اشاره‌گرهایی هستند به ردیف‌های اصلی اطلاعات. به این معنا که این ایندکس‌ها لاگ نشده و همچنین بر روی سخت دیسک ذخیره نمی‌شوند. کار بازسازی مجدد آن‌ها در اولین بار بازیابی بانک اطلاعاتی و آغاز آن به صورت خودکار انجام می‌شود. به همین جهت مباحثی مانند index fragmentation و نگهداری ایندکس‌ها دیگر در اینجا معنا پیدا نمی‌کنند.
دو نوع ایندکس را در اینجا می‌توان تعریف کرد. اولین آن‌ها hash index است و دومین آن‌ها range index. هش ایندکس‌ها برای حالاتی که در کوئری‌ها از عملگر تساوی استفاده می‌شود بسیار مناسب هستند. برای عملگرهای مقایسه‌ای از ایندکس‌های بازه‌ای استفاده می‌شود.
همچنین باید دقت داشت که پس از ایجاد ایندکس‌ها، دیگر امکان تغییر آن‌ها و یا تغییر ساختار جدول ایجاد شده نیست.
همچنین ایندکس‌های تعریف شده در جداول بهینه سازی شده برای حافظه، تنها بر روی ستون‌هایی غیرنال پذیر از نوع BIN2 collation مانند int و datetime قابل تعریف هستند. برای مثال اگر سعی کنیم بر روی ستون Name ایندکسی را تعریف کنیم، به این خطا خواهیم رسید:
 Indexes on character columns that do not use a *_BIN2 collation are not supported with indexes on memory optimized tables.
- در حین تعریف هش ایندکس‌ها، مقدار BUCKET_COUNT نیز باید تنظیم شود. هر bucket توسط مقداری که حاصل هش کردن یک ستون است مشخص می‌شود. کلیدهای منحصربفرد دارای هش‌های یکسان در bucketهای یکسانی ذخیره می‌شوند. به همین جهت توصیه شده‌است که حداقل مقدار bucket تعیین شده در اینجا مساوی یا بیشتر از مقدار تعداد کلیدهای منحصربفرد یک جدول باشد؛ مقدار پیش فرض 2 برابر توسط مایکروسافت توصیه شده‌است.
- نوع‌های قابل تعریف ستون‌ها نیز در اینجا به موارد ذیل محدود هستند و جمع طول آن‌ها از 8060 نباید بیشتر شود:
 bit, tinyint, smallint, int, bigint, money, smallmoney, float, real, datetime, smalldatetime, datetime2,
date, time, numberic, decimal, char(n),  varchar(n) ,nchar(n),  nvarchar(n), sysname, binary(n),
varbinary(n), and Uniqueidentifier


همچنین در management studio، گزینه‌ی جدید new -> memory optimized table نیز اضافه شده‌است و انتخاب آن سبب می‌شود تا قالب T-SQL ایی برای تهیه این نوع جداول، به صورت خودکار تولید گردد.


البته این گزینه تنها برای بانک‌های اطلاعاتی که دارای گروه فایل استریم مخصوص جداول بهینه سازی شده برای حافظه هستند، فعال می‌باشد.


ثبت اطلاعات در جداول معمولی و بهینه سازی شده برای حافظه و مقایسه کارآیی آن‌ها

در مثال زیر، 100 هزار رکورد را در سه جدولی که پیشتر ایجاد کردیم، ثبت کرده و سپس مدت زمان اجرای هر کدام از مجموعه عملیات را بر حسب میلی ثانیه بررسی می‌کنیم:
set statistics time off
SET STATISTICS IO Off
set nocount on
go
-----------------------------

Print 'insert into tblNormal'

DECLARE @start datetime = getdate()
declare @insertCount int = 100000
declare @startId int = 1
declare @customerID int = @startId

while @customerID < @startId + @insertCount
begin
    insert into tblNormal values (@customerID, 'Test', '2013-01-01T00:00:00')
    set @customerID +=1
end

Print DATEDIFF(ms,@start,getdate());
go
-----------------------------

Print 'insert into tblMemoryOptimized_Schema_And_Data'

DECLARE @start datetime = getdate()
declare @insertCount int = 100000
declare @startId int = 1
declare @customerID int = @startId

while @customerID < @startId + @insertCount
begin
    insert into tblMemoryOptimized_Schema_And_Data values (@customerID, 'Test', '2013-01-01T00:00:00')
    set @customerID +=1
end
Print DATEDIFF(ms,@start,getdate());
Go
-----------------------------

Print 'insert into tblMemoryOptimized_Schema_Only'

DECLARE @start datetime = getdate()
declare @insertCount int = 100000
declare @startId int = 1
declare @customerID int = @startId

while @customerID < @startId + @insertCount
begin
    insert into tblMemoryOptimized_Schema_Only values (@customerID, 'Test', '2013-01-01T00:00:00')
    set @customerID +=1
end
Print DATEDIFF(ms,@start,getdate());

Go
با این خروجی تقریبی که بر اساس توانمندی‌های سخت افزاری سیستم می‌تواند متفاوت باشد:
 insert into tblNormal
36423

insert into tblMemoryOptimized_Schema_And_Data
30516

insert into tblMemoryOptimized_Schema_Only
3176
و برای حالت select خواهیم داشت:
 set nocount on
print 'tblNormal'
set statistics time on
select count(CustomerID) from tblNormal
set statistics time off
go
print 'tblMemoryOptimized_Schema_And_Data'
set statistics time on
select count(CustomerID) from tblMemoryOptimized_Schema_And_Data
set statistics time off
go
print 'tblMemoryOptimized_Schema_Only'
set statistics time on
select count(CustomerID) from tblMemoryOptimized_Schema_Only
set statistics time off
go
با این خروجی
 tblNormal
 SQL Server Execution Times:
CPU time = 46 ms,  elapsed time = 52 ms.

tblMemoryOptimized_Schema_And_Data
 SQL Server Execution Times:
CPU time = 32 ms,  elapsed time = 33 ms.

tblMemoryOptimized_Schema_Only
 SQL Server Execution Times:
CPU time = 31 ms,  elapsed time = 30 ms.
تاثیر جداول بهینه سازی شده برای حافظه را در 350K inserts بهتر می‌توان با نمونه‌های متداول مبتنی بر دیسک مقایسه کرد.


برای مطالعه بیشتر

Getting started with SQL Server 2014 In-Memory OLTP
Introduction to SQL Server 2014 CTP1 Memory-Optimized Tables
Overcoming storage speed limitations with Memory-Optimized Tables for SQL Server
Memory-optimized Table – Day 1 Test
Memory-Optimized Tables – Insert Test
Memory Optimized Table – Insert Test …Again
نظرات مطالب
EF Code First #3
اگر بخواهیم فیلدی به اسم Id کلید جدول باشد ولی Identity نباشد چکار باید کرد؟
من میخوام یک سری دیتا رو از یک تیبل دیتابیس قدیمی، منتقل کنم به دیتابیس جدیدم ولی اگر Identity باشه نمیشه دیتا رو Paste کرد توی تیبل دیتابیس جدید.
دیتابیس من SQL CE 4.0 هستش. ممنون.
مطالب
انتخاب نوع داده‌ی مناسب مخصوص ذخیره سازی مقادیر پولی در SQL Server
درحال حاضر، باتوجه به خرده نداشتن مقادیر پولی در ایران، عموما از نوع‌های int و bigint برای ذخیره سازی این مقادیر استفاده می‌شود؛ اما در آینده با احتمال حذف تعدادی از صفرها، نیاز به ثبت خرده‌ها هم ضروری خواهد بود و در اینجا این سؤال مهم مطرح می‌شود که نوع داده‌ای مناسب برای انجام اینکار چیست؟ برای نمونه در SQL Server، نوع‌های داده‌ای decimal، money، smallmoney و امثال آن وجود دارند که در این مطلب، تفاوت‌های مهم آن‌ها و روش صحیح انتخاب نوع داده‌ای مناسب مخصوص اینکار را بررسی خواهیم کرد.


مشکل مهم نوع داده‌ای int جهت ذخیره سازی مقادیر پولی

فرض کنید جدول ساده‌ای را با دو فیلد Id و Price دارید که نوع مبلغ آن‌را با توجه به عدم داشتن خرده در واحد پولی، int انتخاب کرده‌اید:
CREATE TABLE [Test1](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Price] [int] NOT NULL,
 CONSTRAINT [PK_Test1] PRIMARY KEY CLUSTERED 
(
[Id] ASC
));
اگر در این جدول فقط 7 رکورد زیر را ثبت کنیم:
 Insert into Test1 values (1000000000),(1000000000),(1000000000),(1000000000),(1000000000),(1000000000),(1000000000)
به نظر شما خروجی کوئری ساده‌ی زیر که جهت نمایش جمع مبالغ وارد شده تهیه شده، چیست؟
select sum(price) from Test1
خروجی آن فقط استثنای زیر است!
Arithmetic overflow error converting expression to data type int.
عنوان می‌کند که جمع آن از بازه‌ی اعداد صحیح خارج شده‌است و در سیستمی که نوع مبالغ آن‌را int انتخاب کرده‌اید، دیر یا زود به این مشکل خواهید رسید. فقط کافی است کاربران، یکسالی با آن برنامه کار کنند!
برای حل این مشکل می‌توان به صورت موقت، نوع داده‌ای را به bigint تبدیل کرد و مجددا جمع رکوردها را محاسبه کرد:
select sum(cast(price as bigint)) from Test1
یک روش دیگر مواجه شدن با این مساله، عدم انتخاب نوع int برای فیلد Price، از ابتدای کار است.


از نوع داده‌ای float برای ذخیره سازی مقادیر پولی استفاده نکنید!

هیچگاه نباید از نوع داده‌ی float برای ذخیره سازی مقادیر پولی استفاده کرد؛ از این جهت که این نوع اعداد، به صورت تقریبی از یک مقدار decimal و به صورت باینری در SQL Server ذخیره می‌شوند. به همین جهت به محض ذخیره شدن، با عددی غیر دقیق مواجه خواهیم بود. همچنین مقایسه‌ی دقیق این نوع اعداد هم مشکلات خاصی را به همراه دارد.
DECLARE @f AS FLOAT = '29545428.0211111';
SELECT CAST(@f AS NUMERIC(28, 14)) AS value;



SQL Server چگونه مقادیر پولی money و small money را ذخیره می‌کند؟

SQL Server برای کار با مقادیر پولی، دو نوع MONEY و SMALLMONEY را ارائه می‌دهد که شبیه به نوع‌های BIGINT و INT، نیاز به 8 و 4 بایت برای ذخیره سازی دارند. در عمل نوع MONEY شبیه به نوع DECIMAL(19,4) و نوع SMALLMONEY همانند DECIMAL(10,4) رفتار می‌کند. یعنی نوع MONEY می‌تواند تا 15 رقم دسیمال پیش از ممیز و 4 رقم اعشار را ذخیره کند و نوع SMALLMONEY تنها می‌تواند 6 رقم دسیمال و 4 رقم اعشاری را ذخیره کند.
اما ... هرچند نوع داده‌ی MONEY و DECIMAL(19,4) به ظاهر یکی هستند، اما به نحو متفاوتی بر روی دیسک سخت ذخیره می‌شوند. برای نمونه فرض کنید که قصد داریم عدد 4,513.19 را یکبار به صورت MONEY و بار دیگر به صورت SMALLMONEY ذخیره کنیم که در نهایت به جدول زیر می‌رسیم:


همانطور که مشاهده می‌کنید، نوع‌های MONEY و SMALLMONEY، دقیقا همانند BIGINT هشت بایتی و INT، چهار بایتی ذخیره می‌شوند و عملا در پشت صحنه‌ی SQL Server، اعداد صحیح هستند. اما نوع DECIMAL(19,4) که هرچند شبیه به MONEY عمل می‌کند، 9 بایتی است.


الگوریتم انتخاب نوع داده‌ی مناسب ذخیره سازی مقادیر پولی

در فلوچارت زیر که از کتاب «Donald Knuth’s "The Art of Computer Programming – Volume 1".» انتخاب شده، روش مواجه شدن با انواع و اقسام نوع‌های داده‌ای عددی را به خوبی مشخص می‌کند که آیا عدد در حال ذخیره شدن، خرده دارد یا خیر؟ آیا از 922,337,203,685,477.5807 کوچکتر است یا خیر و امثال آن که در تصمیم‌گیری نهایی مؤثر هستند:


اعدادی را که در این نمودار مشاهده می‌کنید، در جدول زیر بهتر توضیح داده شده‌اند. به عبارتی چه تفاوتی بین نوع Money و Decimal(19,4) مشابه وجود دارد:



تفاوت مهم نوع Money و Decimal(19,4)، در دقت آن‌ها است

 تا اینجا به نظر آنچنان تفاوتی بین نوع Money و Decimal(19,4) وجود ندارد و نوع money اتفاقا یک بایت را کمتر اشغال می‌کند و کوچکتر است. اما تفاوت اصلی را با مثال زیر بهتر می‌توان توضیح داد:
CREATE TABLE MoneyTest (
 Mon1 money,
 Mon2 AS Mon1*Mon1,
 Mon3 AS Mon1*Mon1*Mon1,
 Dec1 decimal(19,4),
 Dec2 AS Dec1*Dec1,
 Dec3 AS Dec1*Dec1*Dec1,
 MonDec AS Mon1*Dec1,
 DecMon AS Dec1*Mon1);
در اینجا جدولی تهیه شده که دو ستون اصلی Mon1 و Dec1 را دارد و مابقی ستون‌های آن، محاسباتی هستند:


همانطور که مشاهده می‌کنید، با ضرب دو عدد دسیمال، مقادیر پیش و پس از ممیز، یعنی precision و scale تغییر کرده‌اند، اما در مورد money چنین چیزی رخ نداده و ثابت است. برای مثال زمانیکه با یک عدد DECIMAL(4,2) کار می‌کنیم، اگر آن‌را ضربدر همین عدد کنیم، به یک عدد DECIMAL(8,4) خواهیم رسید که البته حداکثر precision ممکن آن در SQL Server عدد 38 است، اما یک چنین تغییری در حین ضرب اعداد از نوع money رخ نمی‌دهد.

موضوع دقت را با مثال زیر بهتر می‌توان بررسی کرد:
CREATE TABLE [MoneyTest](
[Id] [int] IDENTITY(1,1) NOT NULL,
decimalMoney decimal(19,4),
moneyMoney money
 CONSTRAINT [PK_MoneyTest] PRIMARY KEY CLUSTERED 
(
[Id] ASC
));
فرض کنید جدولی را داریم با دو فیلد از نوع Money و مشابه آن یعنی decimal(19,4) به صورت فوق. اگر رکوردهای زیر را به آن اضافه کنیم:
INSERT INTO MoneyTest
VALUES
(12321423442.3456,12321423442.3456),
(1111111.1919,1111111.1919)
و سپس سعی کنیم که جمع اعداد وارد شده را محاسبه کنیم:
SELECT * FROM MoneyTest

SELECT SUM(decimalMoney) AS [sumDecimal],
   SUM(moneyMoney) AS [sumMoney]
FROM MoneyTest
به نتیجه‌ی زیر می‌رسیم:


همانطور که مشخص است در حین محاسباتی مانند جمع و منها و محاسبه‌ی sum، تفاوتی بین این نوع‌ها نیست. اما اگر سعی در تقسیم آن‌ها کنیم:
DECLARE @moneyPer money,
  @decimalPer decimal(19,4)
SET @moneyPer = (SELECT moneyMoney FROM MoneyTest WHERE id = 2)/((SELECT moneyMoney FROM MoneyTest WHERE id = 1))
SET @decimalPer = (SELECT decimalMoney FROM MoneyTest WHERE id = 2)/((SELECT decimalMoney FROM MoneyTest WHERE id = 1))
SELECT @moneyPer AS[moneyPer], @decimalPer AS [decimalPer];
به خروجی زیر می‌رسیم:


نتیجه‌ی واقعی 0,00009 است که پس از گرد شدن، به 0.0001 مقدار دسیمال می‌رسیم، اما این دقت در نوع money از دست رفته‌است.

نکته‌ی مهمی که در اینجا قابل مشاهد‌ه‌است، محدود نبودن نتیجه‌ی حاصل، به دقت اعشارها در عدد decimal تعریف شده و scale تعریف شده‌ی اولیه‌ی آن است. نمونه‌ی دیگر آن‌را در مثال زیر می‌توانید مشاهده کنید که هرچند عدد دسیمال تعریف شده، فقط 2 رقم اعشاری دارد، اما در حین تقسیم، از این مساله صرفنظر شده و خروجی آن محدود به 2 رقم اعشار نیست؛ برخلاف نوع money که حداکثر 4 رقم ثابت اعشاری را بیشتر نمی‌تواند داشته باشد:
DECLARE @M MONEY = 1234, @D DECIMAL(6,2) = 1234
SELECT @M/$1000000 AS [MONEY] ,
 @D/$1000000 AS [DECIMAL]



نتیجه‌گیری

برای ذخیره سازی مقادیر پولی در SQL Server، اگر سیستم شما OLTP-like است و با اعدادی مانند 1000.24 کار می‌کنید و حداکثر می‌خواهید جمع و منهای آن‌‌ها را محاسبه کنید، انتخاب نوع  MONEY و یا  SMALLMONEY بسیار مناسب است؛ اما اگر سیستم شما OLAP-like است و در آن اعمال ضرب و تقسیم زیاد رخ می‌دهد، فقط از نوع Decimal استفاده کنید.


DECLARE @dOne DECIMAL(19,4) = 1,
  @dThree DECIMAL(19,4) = 3,
  @mOne MONEY = 1,
  @mThree MONEY = 3

SELECT (@dOne/@dThree) * @dThree AS DecimalResult,
  (@mOne/@mThree) * @mThree AS MoneyResult
مطالب
مقایسه حساس به حروف کوچک و بزرگ در SQL Server

چندین روش برای انجام مقایسه حساس به حروف کوچک و بزرگ (case sensitive) در SQL Server وجود دارد که در ادامه آن‌ها را مرور خواهیم کرد:

ابتدا جدول موقتی زیر را جهت آزمایشات بعدی در نظر بگیرید

CREATE TABLE #tblTest
(
f1 NVARCHAR(50)
)

INSERT INTO #tblTest (f1) VALUES('Test1')
INSERT INTO #tblTest (f1) VALUES('TEST1')

الف) استفاده از collation صحیح
عموما هنگام نصب اس کیوال سرور از collation غیرحساس به کوچکی و بزرگی حروف استفاده می‌شود و این مورد سبب می‌شود که پیش فرض ایجاد دیتابیس‌ها نیز به همین صورت باشد (هر چند کاملا قابل کنترل و تنظیم است). به صورت پویا می‌توان این collation را در کوئری‌ها نیز اعمال نمود. برای مثال:

SELECT f1 FROM #tblTest WHERE f1 COLLATE SQL_Latin1_General_CP1_CS_AS = 'Test1'
در این مثال اگر از collation حساس به کوچکی و بزرگی حروف استفاده نمی‌شد، خروجی هر دو رکورد ثبت شده می‌بود.

ب) استفاده از تابع BINARY_CHECKSUM اس کیوال سرور (نوعی الگوریتم ویژه، شبیه به امضای دیجیتال و هش کردن اطلاعات است)

SELECT f1 FROM #tblTest WHERE BINARY_CHECKSUM(f1) = BINARY_CHECKSUM('Test1')
و یا استفاده از امکانات هش کردن اطلاعات در اس کیوال سرور: (امضای دیجیتال دو رشته با هم مقایسه می‌شوند)

SELECT f1 FROM #tblTest WHERE hashbytes('md5',f1) = hashbytes('md5',N'Test1')
ج) مقایسه در حالت بایناری

SELECT f1 FROM #tblTest WHERE convert(varbinary(50),f1) = convert(varbinary(50),N'Test1')