در انتهای قسمت قبل، نحوهی ایجاد یک جدول جدید با فیلدی از نوع فایل استریم بررسی شد، حال اگر جدولی از پیش وجود داشت، نحوهی افزودن فیلد ویژه مورد نظر به آن، به صورت زیر است:
alter table tbl_files set(filestream_on ='default')
go
alter table tbl_files
add
[systemfile] varbinary(max) filestream null ,
FileId uniqueidentifier not null rowguidcol unique default (newid())
go
در ادامه جدول tblFiles قسمت قبل را در نظر بگیرید:
CREATE TABLE [tblFiles](
[FileId] [uniqueidentifier] ROWGUIDCOL NOT NULL,
[Title] [nvarchar](255) NOT NULL,
[SystemFile] [varbinary](max) FILESTREAM NULL,
UNIQUE NONCLUSTERED
(
[FileId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] FILESTREAM_ON [fsg1]
ALTER TABLE [dbo].[tblFiles] ADD DEFAULT (newid()) FOR [FileId]
GO
نحوهی افزودن رکوردی جدید به جدول tblFiles :
INSERT INTO [tblFiles]
(
[Title],
[SystemFile]
)
VALUES
(
'file-1',
CAST('data data data' AS VARBINARY(MAX))
)
اگر کنجکاو باشید که این فایل اکنون کجا ذخیره شده و نحوهی مدیریت آن توسط اس کیوال سرور به چه صورتی است، فقط کافی است به مسیری که هنگام افزودن گروه فایلها و فایل مربوطه در تنظیمات خواص دیتابیس در قسمت قبل مشخص کردیم، مراجعه کرد (شکل زیر).
بدیهی است افزودن یک رشته به این صورت کاربرد عملی ندارد و صرفا جهت یک مثال ارائه شد. در ادامه، نحوهی ثبت محتویات یک فایل را در فیلدی از نوع فایل استریم و سپس خواندن اطلاعات آنرا از طریق برنامه نویسی بررسی خواهیم کرد:
using System;
using System.IO;
using System.Data.SqlClient;
using System.Data;
namespace FileStreamTest
{
class CFS
{
/// <summary>
/// افزودن رکورد به جدول حاوی ستونی از نوع فایل استریم
/// </summary>
/// <param name="filePath">مسیر فایل</param>
/// <param name="title">عنوانی دلخواه</param>
public static void AddNewRecord(string filePath, string title)
{
//آیا فایل وجود دارد؟
if (!File.Exists(filePath))
throw new FileNotFoundException(
"لطفا مسیر فایل معتبری را مشخص نمائید", filePath);
//خواندن اطلاعات فایل در آرایهای از بایتها
byte[] buffer = File.ReadAllBytes(filePath);
using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();
//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//ساخت عبارت افزودن پارامتری
using (SqlCommand objSqlCmd = new SqlCommand(
"INSERT INTO [tblFiles]([Title],[SystemFile]) VALUES(@title , @file)",
objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.Text;
//تعریف وضعیت پارامترها و مقدار دهی آنها
objSqlCmd.Parameters.AddWithValue("@title", title);
objSqlCmd.Parameters.AddWithValue("@file", buffer);
//اجرای فرامین
objSqlCmd.ExecuteNonQuery();
}
//پایان تراکنش
objSqlTran.Commit();
}
}
}
/// <summary>
/// دریافت اطلاعات فایل ذخیره شده به صورت آرایهای از بایتها
/// </summary>
/// <param name="fileId">کلید مورد استفاده</param>
/// <returns></returns>
public static byte[] GetDataFromDb(string fileId)
{
byte[] data = null;
using (SqlConnection objConn = new SqlConnection())
{
//کوئری اس کیوال پارامتری جهت دریافت محتویات فایل
string cmdText = "SELECT SystemFile FROM tblFiles WHERE FileId=@id";
using (SqlCommand objCmd = new SqlCommand(cmdText, objConn))
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objConn.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objConn.Open();
//تنظیم کردن وضعیت و مقدار پارامتر تعریف شده در کوئری
objCmd.Parameters.AddWithValue("@id", fileId);
//اجرای فرامین و دریافت فایل
using (SqlDataReader objread = objCmd.ExecuteReader())
{
if (objread != null)
if (objread.Read())
{
if (objread["SystemFile"] != DBNull.Value)
data = (byte[])objread["SystemFile"];
}
}
}
}
return data;
}
}
}
مثالی در مورد روش استفاده از کلاس فوق :
using System.IO;
namespace FileStreamTest
{
class Program
{
static void Main(string[] args)
{
CFS.AddNewRecord(@"C:\filest05.PNG", "test1");
//آی دی رکورد ذخیره شده در دیتابیس برای مثال
byte[] data = CFS.GetDataFromDb("BB848D45-382C-4D95-BF4E-52C3509407D4");
if (data != null)
{
File.WriteAllBytes(@"C:\tst.PNG", data);
}
}
}
}
در روش دیگری که در اکثر مقالات مرتبط مورد استفاده است، از شیء SqlFileStream کمک گرفته شده و نحوهی انجام آن نیز به صورت زیر میباشد.
در ابتدا دو رویه ذخیره شده زیر را ایجاد میکنیم:
CREATE PROCEDURE [AddFile](@Title NVARCHAR(255), @filepath VARCHAR(MAX) OUTPUT)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @ID UNIQUEIDENTIFIER
SET @ID = NEWID()
INSERT INTO [tblFiles]
(
[FileId],
[title],
[SystemFile]
)
VALUES
(
@ID,
@Title,
CAST('' AS VARBINARY(MAX))
)
SELECT @filepath = SystemFile.PathName()
FROM tblFiles
WHERE FileId = @ID
END
GO
CREATE PROCEDURE [GetFilePath](@Id VARCHAR(50))
AS
BEGIN
SET NOCOUNT ON;
SELECT SystemFile.PathName()
FROM tblFiles
WHERE FileId = @ID
END
رویه ذخیره شده GetFilePath نیز تنها مسیر سیستمی فایل استریم ذخیره شده را بر میگرداند.
به این ترتیب کدهای برنامه به صورت زیر تغییر خواهند کرد:
using System.Data.SqlClient;
using System.Data;
using System.Data.SqlTypes;
using System.IO;
namespace FileStreamTest
{
class CFSqlFileStream
{
/// <summary>
/// افزودن رکورد به جدول حاوی ستونی از نوع فایل استریم
/// </summary>
/// <param name="filePath">مسیر فایل</param>
/// <param name="title">عنوانی دلخواه</param>
public static void AddNewRecord(string filePath, string title)
{
//آیا فایل وجود دارد؟
if (!File.Exists(filePath))
throw new FileNotFoundException(
"لطفا مسیر فایل معتبری را مشخص نمائید", filePath);
//خواندن اطلاعات فایل در آرایهای از بایتها
byte[] buffer = File.ReadAllBytes(filePath);
using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();
//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//استفاده از رویه ذخیره شده افزودن فایل
using (SqlCommand objSqlCmd = new SqlCommand(
"AddFile", objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.StoredProcedure;
//مشخص ساختن وضعیت و مقدار پارامتر عنوان
SqlParameter objSqlParam1 = new SqlParameter("@Title", SqlDbType.NVarChar, 255);
objSqlParam1.Value = title;
//مشخص ساختن پارامتر خروجی رویه ذخیره شده
SqlParameter objSqlParamOutput = new SqlParameter("@filepath", SqlDbType.VarChar, -1);
objSqlParamOutput.Direction = ParameterDirection.Output;
//افزودن پارامترها به شیء کامند
objSqlCmd.Parameters.Add(objSqlParam1);
objSqlCmd.Parameters.Add(objSqlParamOutput);
//اجرای رویه ذخیره شده
objSqlCmd.ExecuteNonQuery();
//و سپس دریافت خروجی آن
string Path = objSqlCmd.Parameters["@filepath"].Value.ToString();
//زمینه تراکنش فایل استریم موجود را دریافت کرده و از آن برای نوشتن محتویات فایل استفاده خواهیم کرد
//این مورد نیز یکی از تازههای اس کیوال سرور 2008 است
using (SqlCommand objCmd = new SqlCommand(
"SELECT GET_FILESTREAM_TRANSACTION_CONTEXT()", objSqlCon, objSqlTran))
{
byte[] objContext = (byte[])objCmd.ExecuteScalar();
using (SqlFileStream objSqlFileStream =
new SqlFileStream(Path, objContext, FileAccess.Write))
{
objSqlFileStream.Write(buffer, 0, buffer.Length);
}
}
}
objSqlTran.Commit();
}
}
}
/// <summary>
/// دریافت اطلاعات فایل ذخیره شده به صورت آرایهای از بایتها
/// </summary>
/// <param name="fileId">کلید مورد استفاده</param>
/// <returns></returns>
public static byte[] GetDataFromDb(string fileId)
{
byte[] buffer = null;
using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();
//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//استفاده از رویه ذخیره شده دریافت مسیر فایل
using (SqlCommand objSqlCmd =
new SqlCommand("GetFilePath", objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.StoredProcedure;
//مشخص ساختن پارامتر ورودی رویه ذخیره شده و مقدار دهی آن
SqlParameter objSqlParam1 = new SqlParameter("@ID", SqlDbType.VarChar, 50);
objSqlParam1.Value = fileId;
objSqlCmd.Parameters.Add(objSqlParam1);
//اجرای رویه ذخیره شده و دریافت مسیر سیستمی فایل استریم
string path = string.Empty;
using (SqlDataReader sdr = objSqlCmd.ExecuteReader())
{
sdr.Read();
path = sdr[0].ToString();
}
//زمینه تراکنش فایل استریم موجود را دریافت کرده و از آن برای خواندن محتویات فایل استفاده خواهیم کرد
//این مورد نیز یکی از تازههای اس کیوال سرور 2008 است
using (SqlCommand objCmd = new SqlCommand(
"SELECT GET_FILESTREAM_TRANSACTION_CONTEXT()", objSqlCon, objSqlTran))
{
byte[] objContext = (byte[])objCmd.ExecuteScalar();
using (SqlFileStream objSqlFileStream =
new SqlFileStream(path, objContext, FileAccess.Read))
{
buffer = new byte[(int)objSqlFileStream.Length];
objSqlFileStream.Read(buffer, 0, buffer.Length);
}
}
}
objSqlTran.Commit();
}
}
return buffer;
}
}
}
FILESTREAM Storage in SQL Server 2008
MVC Scaffolding #3
آشنایی با ساختار اصلی MVC Scaffolding
پس از نصب MVC Scaffolding از طریق NuGet به پوشه Packages مراجعه نمائید. در اینجا پوشههای MvcScaffolding، T4Scaffolding و T4Scaffolding.Core ساختار اصلی این بسته را تشکیل میدهند. برای نمونه اگر پوشه T4Scaffolding\tools را باز کنیم، شاهد تعدادی فایل ps1 خواهیم بود که همان فایلهای پاورشل هستند. مطابق طراحی NuGet، همواره فایلی با نام init.ps1 در ابتدا اجرا خواهد شد. همچنین در اینجا پوشههای T4Scaffolding\tools\EFRepository و T4Scaffolding\tools\EFDbContext نیز قرار دارند که حاوی قالبهای اولیه کدهای مرتبط با الگوی مخزن و DbContext تولیدی میباشند.
در پوشه MvcScaffolding\tools، ساختار قالبهای پیش فرض تولید Viewها و کنترلرهای تولیدی قرار دارند. در اینجا به ازای هر مورد، دو نگارش vb و cs قابل مشاهده است.
سفارشی سازی قالبهای پیش فرض Viewهای MVC Scaffolding
برای سفارشی سازی قالبهای پیش فرض از دستور کلی زیر استفاده میشود:
Scaffold CustomTemplate Name Template
Scaffold CustomTemplate View Index
اگر دستور فوق را اجرا کنیم، فایل جدیدی به نام CodeTemplates\Scaffolders\MvcScaffolding.RazorView\Index.cs.t4 به پروژه جاری اضافه میشود. از این پس کلیه فرامین اجرایی، از نسخه محلی فوق بجای نمونههای پیش فرض استفاده خواهند کرد.
در ادامه قصد داریم اندکی این قالب پیش فرض را جهت اعمال ویژگی DisplayName به هدر جدول تولیدی نمایش اطلاعات Tasks تغییر دهیم. در کلاس Task، خاصیت زمان موعود با ویژگی DisplayName مزین شده است. این نام نمایشی حین تولید فرمهای ثبت و ویرایش اطلاعات بکار گرفته میشود، اما در زمان تولید جدول اطلاعات ثبت شده، به هدر جدول اعمال نمیگردد.
[DisplayName("Due Date")] public DateTime? DueDate { set; get; }
// Describes the information about a property on the model class ModelProperty { public string Name { get; set; } public string DisplayName { get; set; } public string ValueExpression { get; set; } public EnvDTE.CodeTypeRef Type { get; set; } public bool IsPrimaryKey { get; set; } public bool IsForeignKey { get; set; } public bool IsReadOnly { get; set; } }
static string GetDisplayName(EnvDTE.CodeProperty prop) { var displayAttr = prop.Attributes.OfType<EnvDTE80.CodeAttribute2>().Where(x => x.FullName == typeof(System.ComponentModel.DisplayNameAttribute).FullName).FirstOrDefault(); if(displayAttr == null) { return prop.Name; } return displayAttr.Value.Replace("\"",""); }
اکنون برای اعمال متد GetDisplayName، متد GetEligibleProperties را یافته و به نحو زیر تغییر دهید:
results.Add(new ModelProperty { Name = prop.Name, DisplayName = GetDisplayName(prop), ValueExpression = "Model." + prop.Name, Type = prop.Type, IsPrimaryKey = Model.PrimaryKeyName == prop.Name, IsForeignKey = ParentRelations.Any(x => x.RelationProperty == prop), IsReadOnly = !prop.IsWriteable() });
اکنون قسمت هدر جدول تولیدی را در ابتدای فایل t4 یافته و به نحو زیر تغییر میدهیم تا از DisplayName استفاده کند:
<# List<ModelProperty> properties = GetModelProperties(Model.ViewDataType, true); foreach (ModelProperty property in properties) { if (!property.IsPrimaryKey && !property.IsForeignKey) { #> <th> <#= property.DisplayName #> </th> <# } } #>
PM> Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
سفارشی سازی قالبهای پیش فرض کنترلرهای MVC Scaffolding
در ادامه قصد داریم کدهای الگوی مخزن تهیه شده را اندکی تغییر دهیم. برای مثال با توجه به اینکه از تزریق وابستگیها استفاده خواهیم کرد، نیازی به سازنده اولیه پیش فرض کنترلر که در بالای آن ذکر شده «در صورت استفاده از یک DI این مورد را حذف کنید»، نداریم. برای این منظور دستور زیر را اجرا کنید:
PM> Scaffold CustomTemplate Controller ControllerWithRepository
به این ترتیب فایل جدید CodeTemplates\Scaffolders\MvcScaffolding.Controller\ControllerWithRepository.cs.t4 به پروژه جاری اضافه خواهد شد. در این فایل چند سطر ذیل را یافته و سپس حذف کنید:
// If you are using Dependency Injection, you can delete the following constructor public <#= Model.ControllerName #>() : this(<#= String.Join(", ", Repositories.Values.Select(x => "new " + x.RepositoryTypeName + "()")) #>) { }
PM> Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force -ForceMode ControllerOnly
یا اگر نیاز به تغییر کدهای الگوی مخزن مورد استفاده است میتوان از دستور ذیل استفاده کرد:
Scaffold CustomScaffolder EFRepository
لیست Scaffolderهای مهیا با دستور Get-Scaffolder قابل مشاهده است.
امنیت در LINQ to SQL
جواب کوتاه: بسیار زیاد!
توضیحات:
string query = @"SELECT * FROM USER_PROFILE
WHERE LOGIN_ID = '"+loginId+@"' AND PASSWORD = '"+password+@"'";
protected void btnSearch_Click(object sender, EventArgs e)
{
String cmd = @"SELECT [CustomerID], [CompanyName], [ContactName]
FROM [Customers] WHERE CompanyName ='" + txtCompanyName.Text
+ @"'";
SqlDataSource1.SelectCommand = cmd;
GridView1.Visible = true;
}
راه حلی که برای مقابله با آن در دات نت ارائه شده نوشتن کوئریهای پارامتری است و در این حالت کار encoding اطلاعات ورودی به صورت خودکار توسط فریم ورک مورد استفاده انجام خواهد شد؛ همچنین برای مثال اس کیوال سرور، execution plan این نوع کوئریهای پارامتری را همانند رویههای ذخیره شده، کش کرده و در دفعات آتی فراخوانی آنها به شدت سریعتر عمل خواهد کرد. برای مثال:
SqlCommand cmd = new SqlCommand("SELECT UserID FROM Users WHERE UserName=@UserName AND Password=@Password");
cmd.Parameters.Add(new SqlParameter("@UserName", System.Data.SqlDbType.NVarChar, 255, UserName));
cmd.Parameters.Add(new SqlParameter("@Password", System.Data.SqlDbType.NVarChar, 255, Password));
dr = cmd.ExecuteReader();
if (dr.Read()) userId = dr.GetInt32(dr.GetOrdinal("UserID"));
اما در مورد LINQ to SQL چطور؟
این سیستم به صورت پیش فرض طوری طراحی شده است که تمام کوئریهای SQL نهایی حاصل از کوئریهای LINQ نوشته شده توسط آن، پارامتری هستند. به عبارت دیگر این سیستم به صورت پیش فرض برای افرادی که دارای حداقل اطلاعات امنیتی هستند به شدت امنیت بالایی را به همراه خواهد آورد.
برای مثال کوئری LINQ زیر را در نظر بگیرید:
var products = from p in db.products
where p.description.StartsWith(_txtSearch.Text)
select new
{
p.description,
p.price,
p.stock
};
exec sp_executesql N'SELECT [t0].[description], [t0].[price], [t0].[stock]
FROM [dbo].[products] AS [t0]
WHERE [t0].[description] LIKE @p0',N'@p0 varchar(5)',@p0='sony%'
db.Log = Console.Out;
همانطور که ملاحظه میکنید، کوئری نهایی تولید شده پارامتری است و در صورت ورود اطلاعات خطرناک در پارامتر p0 ، هیچ اتفاق خاصی نخواهد افتاد و صرفا رکوردی بازگشت داده نمیشود.
و یا همان مثال کلاسیک اعتبار سنجی کاربر را در نظر بگیرید:
public bool Validate(string loginId, string password)
{
DataClassesDataContext db = new DataClassesDataContext();
var validUsers = from user in db.USER_PROFILEs
where user.LOGIN_ID == loginId
&& user.PASSWORD == password
select user;
if (validUsers.Count() > 0) return true;
else return false;
}
SELECT [t0].[LOGIN_ID], [t0].[PASSWORD]
FROM [dbo].[USER_PROFILE] AS [t0]
WHERE ([t0].[LOGIN_ID] = @p0) AND ([t0].[PASSWORD] = @p1)
تذکر مهم هنگام استفاده از سیستم LINQ to SQL :
اگر با استفاده از LINQ to SQL مجددا به روش قدیمی اجرای مستقیم کوئریهای SQL خود همانند مثال زیر روی بیاورید (این امکان نیز وجود دارد)، نتیجه این نوع کوئریهای حاصل از جمع زدن رشتهها، پارامتری "نبوده" و مستعد به تزریق اس کیوال هستند:
string sql = "select * from Trade where DealMember='" + this.txtParams.Text + "'";
var trades = driveHax.ExecuteQuery<Trade>(sql);
اما روش صحیحی نیز در مورد بکارگیری متد ExecuteQuery وجود دارد. استفاده از این متد به شکل زیر مشکل را حل خواهد کرد:
IEnumerable<Customer> results = db.ExecuteQuery<Customer>(
"SELECT contactname FROM customers WHERE city = {0}", "Tehran");
یک. قبل از آپلود امکان برش وجود داشته باشد.
دو. سیستم برش قابلیت پشتیبانی از تناسب بین پهنا و ارتفاع را داشته باشد.
سه. قابلیت استفاده خارج از قواعد Ajax را داشته باشد و بتوان ارسال آن را به طور دستی کنترل کرد.
چهار. پشتیبانی برش تصاویر به صورت تاچ برای گوشیهای همراه را نیز دارا باشد.
کتابخانه jcrop کتابخانهای است که این امکانات را برای شما فراهم میکند. این کتابخانه بدین صورت است که در حین برش به شما 4 عدد x1,x2,y1,y2 را داده و شما با ارسال آن به سمت سرور میتوانید بر اساس این اعداد، عکس اصلی را برش بزنید. بدین صورت شما هم عکس اصلی را دارید و هم مختصات برش را دارید و اگر دوست دارید در جاهای مختلف از عکس اصلی برش داشته باشید، بسیار مفید خواهد بود.
مرحله اول:
ابتدا فایل jcrop را دانلود نمایید.
مرحله دوم:
کد Html زیر را به صفحه اضافه کنید:
<div> <!-- upload form --> <!-- hidden crop params --> <input type="hidden" id="x1" name="x1" /> <input type="hidden" id="y1" name="y1" /> <input type="hidden" id="x2" name="x2" /> <input type="hidden" id="y2" name="y2" /> <h2>ابتدا تصویر خود را انتخاب کنید</h2> <div><input type="file" name="postedFileBase" data-buttonText="انتخاب تصویر" id="image_file" onchange="fileSelectHandler()" /></div> <div></div> <div> <h2>قسمتی از تصویر را انتخاب نمایید</h2> <img id="preview" /> <div> <label>حجم فایل </label> <input type="text" id="filesize" name="filesize" /> <label>نوع فایل</label> <input type="text" id="filetype" name="filetype" /> <label>ابعاد فایل</label> <input style="direction: ltr;" type="text" id="filedim" name="filedim" /> </div> </div> </div>
تگ step2 نیز بعد از نمایش موفقیت آمیز تصویر نشان داده میشود که کاربر میتواند در آن تصویر را برش دهد و شامل بخش info نیز میباشد تا بتوان اندازه اصلی تصویر، نوع فایل تصویر Content Type و حجم آن را نمایش داد.
مرحله سوم:
سپس برای استایل دهی کدهای بالا از کد Css زیر استفاده میکنیم:
.bheader { background-color: #DDDDDD; border-radius: 10px 10px 0 0; padding: 10px 0; text-align: center; } .bbody { color: #000; overflow: hidden; padding-bottom: 20px; text-align: center; background: -moz-linear-gradient(#ffffff, #f2f2f2); background: -ms-linear-gradient(#ffffff, #f2f2f2); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f2f2f2)); background: -webkit-linear-gradient(#ffffff, #f2f2f2); background: -o-linear-gradient(#ffffff, #f2f2f2); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f2f2f2'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f2f2f2')"; background: linear-gradient(#ffffff, #f2f2f2); } .bbody h2, .info, .error { margin: 10px 0; } .step2, .error { display: none; } .error { color: red; } .info { } label { margin: 0 5px; } .roundinput { border: 1px solid #CCCCCC; border-radius: 10px; padding: 4px 8px; text-align: center; width: 150px; } .jcrop-holder { display: inline-block; } input[type=submit] { background: #e3e3e3; border: 1px solid #bbb; border-radius: 3px; -webkit-box-shadow: inset 0 0 1px 1px #f6f6f6; box-shadow: inset 0 0 1px 1px #f6f6f6; color: #333; padding: 8px 0 9px; text-align: center; text-shadow: 0 1px 0 #fff; width: 150px; } input[type=submit]:hover { background: #d9d9d9; -webkit-box-shadow: inset 0 0 1px 1px #eaeaea; box-shadow: inset 0 0 1px 1px #eaeaea; color: #222; cursor: pointer; } input[type=submit]:active { background: #d0d0d0; -webkit-box-shadow: inset 0 0 1px 1px #e3e3e3; box-shadow: inset 0 0 1px 1px #e3e3e3; color: #000; }
مرحله چهارم:
افزودن کد جاوااسکریپتی زیر برای کار کردن با کتابخانه Jcrop میباشد:
function bytesToSize(bytes) { var sizes = ['بایت', 'کیلو بایت', 'مگابایت']; if (bytes == 0) return 'n/a'; var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i]; }; function updateInfo(e) { $('#x1').val(e.x); $('#y1').val(e.y); $('#x2').val(e.x2); $('#y2').val(e.y2); }; var jcrop_api, boundx, boundy; function fileSelectHandler() { var oFile = $('#image_file')[0].files[0]; $('.error').hide(); var rFilter = /^(image\/jpeg|image\/png)$/i; if (!rFilter.test(oFile.type)) { $('.error').html('فقط تصویر معتبر انتخاب نمایید').show(); return; } var oImage = document.getElementById('preview'); var oReader = new FileReader(); oReader.onload = function (e) { oImage.src = e.target.result; oImage.onload = function () { $('.step2').fadeIn(500); var sResultFileSize = bytesToSize(oFile.size); $('#filesize').val(sResultFileSize); $('#filetype').val(oFile.type); $('#filedim').val(oImage.naturalWidth + ' x ' + oImage.naturalHeight); if (typeof jcrop_api != 'undefined') { jcrop_api.destroy(); jcrop_api = null; $('#preview').width(oImage.naturalWidth); $('#preview').height(oImage.naturalHeight); } $('#preview').Jcrop({ aspectRatio: 2, bgFade: true, bgOpacity: .3, onChange: updateInfo, onSelect: updateInfo }, function () { //var bounds = this.getBounds(); //var boundx = bounds[0]; //var boundy = bounds[1]; // Store the Jcrop API in the jcrop_api variable jcrop_api = this; }); }; }; oReader.readAsDataURL(oFile); }
تابع fileSelectHandler
function fileSelectHandler() { var oFile = $('#image_file')[0].files[0]; $('.error').hide(); var rFilter = /^(image\/jpeg|image\/png)$/i; if (!rFilter.test(oFile.type)) { $('.error').html('فقط تصویر معتبر انتخاب نمایید').show(); return; }
در ادامه همین تابع بالا، کدهای زیر را اضافه میکنیم:
var oImage = document.getElementById('preview'); var oReader = new FileReader(); oReader.onload = function (e) { oImage.src = e.target.result; oImage.onload = function () { $('.step2').fadeIn(500); var sResultFileSize = bytesToSize(oFile.size); $('#filesize').val(sResultFileSize); $('#filetype').val(oFile.type); $('#filedim').val(oImage.naturalWidth + ' x ' + oImage.naturalHeight); if (typeof jcrop_api != 'undefined') { jcrop_api.destroy(); jcrop_api = null; $('#preview').width(oImage.naturalWidth); $('#preview').height(oImage.naturalHeight); } $('#preview').Jcrop({ aspectRatio: 2, bgFade: true, bgOpacity: .3, onChange: updateInfo, onSelect: updateInfo, onRelease: clearInfo }, function () { //var bounds = this.getBounds(); //var boundx = bounds[0]; //var boundy = bounds[1]; jcrop_api = this; }); }; }; oReader.readAsDataURL(oFile);
FileReader یکی از توابع موجود در HTML است که مستندات آن در سایت موزیلا موجود است و قابلیت خواندن غیرهمزمان فایلها و اشیا Blob را دارد. در خط آخر به عنوان پارامتر ما فایلی را که در آپلودر خوانده ایم و در مرحله قبل نوع فایل آن را بررسی کردیم، پاس میکنیم و باعث میشود که رویداد Load شیء FileReader صدا زده شود.
در این رویداد ابتدا اطلاعات این فایل را از قبیل سایز و ابعاد و نوع فایل، خوانده و در همان تگ Div که با کلاس info تعیین شده بود، نمایش میدهیم. سپس متغیر jcrop_api را که به صورت global در بالای تابع صدا زدیم، بررسی میکنم که آیا از قبل پر شدهاست یا خیر؟ اگر از قبل پرشدهاست باید شیء Jcrop را که به آن اعمال شده است، نابود و آن را نال کنیم تا برای تصویر جدید آماده شود. این کد زمانی کاربرد دارد که کاربر از تصویر قبلی انصراف دادهاست و تصویر جدیدی را انتخاب نموده است یا اینکه عملیات دارد به صورت ایجکسی پیاده میشود. اگر عملیات نابودی روی این پلاگین صورت نگیرد، برای مرتبه دوم کار نخواهد کرد.
سپس پلاگین جیکوئری Jcrop را بر روی آن اعمال میکنیم. در پرامتر اول یک سری تنظیمات اولیه را انجام میدهیم که در ادامه با آن آشنا میشویم و در پارامتر دوم یک callback را به آن پاس میکنیم تا بعد از آماده شدن پلاگین اجرا شود که در آن شیء جدید ایجاد شده یعنی this را در متغیری به اسم jcrop_api دخیره میکنیم تا در بررسیهای آتی که در بند بالا توضیح داده شد، در دسترس داشته باشیم. همچنین در این تابع شما میتوانید اندازه تصویر انتخابی را نیز داشته باشید.
این پلاگین شامل optionهای متفاوتی در پارامتر اول است که آنها را بررسی میکنیم:
MinSize : شما میتوانید حداقل پهنا و ارتفاعی را برای برش زدن تصویر در نظر بگیرید.
minSize:[40,20]
aspectRatio:1.5
bgOpacity از 0 تا یک مقدار میگیرد و میزان opacity محلهای تاریک را تعیین میکند. همچنین شامل سه رویداد onSelect,onChange,onrelease هم میباشد که به ترتیب در موارد زیر رخ میدهند:
ناحیه مورد نظر انتخاب شد.
ناحیه مورد نظر در حالت انتخاب است و ماوس در حال درگ شدن است و با هر حرکتی ماوس اجرا میگردد.
ناحیه انتخابی از حالت انتخاب خارج شد.
دو رویداد اول یعنی onchange و onSelect را برای به روزسانی فیلدهای مخفی و مختصات استفاده میکنیم:
function updateInfo(e) { $('#x1').val(e.x); $('#y1').val(e.y); $('#x2').val(e.x2); $('#y2').val(e.y2); };
این مختصات از طریق یک پارامتر به آنها پاس میشود. به غیر از این چهار عدد مختصات میتوانید با استفاده از متغیرهای w و h هم اندازه پهنا و ارتفاع محل برش خورده را نیز به دست آورید. هر چند که این اعداد، از تفریق خود مختصات هم به دست میآیند.
یک تابع جزئی دیگر هم در این فایل وجود دارد که حین نمایش اندازه تصویر، واحد نمایش مناسب آن را برای ما انتخاب میکند:
function bytesToSize(bytes) { var sizes = ['بایت', 'کیلو بایت', 'مگابایت']; if (bytes == 0) return 'n/a'; var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + sizes[i]; };
بعد از اینکه کدهای سمت کلاینت را تمام کردیم لازم است با نحوه برش تصویر در سمت سرور هم آشنا شویم:
public static byte[] Resize(this byte[] byteImageIn, int x1,int y1,int x2,int y2) { ImageConverter ic = new ImageConverter(); Image src = (Image)(ic.ConvertFrom(byteImageIn)); Bitmap target = new Bitmap(x2 - x1, y2 - y1); using (Graphics graphics = Graphics.FromImage(target)) graphics.DrawImage(src, new Rectangle(0, 0, target.Width, target.Height), new Rectangle(x1,y1,x2-x1,y2-y1), GraphicsUnit.Pixel); src = target; using (var ms = new MemoryStream()) { src.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); return ms.ToArray(); } }
از آنجا که ما تصاویر را در دیتابیس به صورت آرایهای از بایتها ذخیره میکنیم، extension method ذکر شده در بالا تصویر را در حالت آرایهای از بایتها برش میدهد. بدیهی که بسته به نیاز شما کد بالا دست خوش تغییراتی خواهد شد. ابتدا تصویر باینری را به شی Image تبدیل میکنیم و یک شیء Bitmap جدید را به عنوان بوم خالی و به اندازه کادر برش ایجاد میکنیم تا تصویر برش خورده در آن قرار بگیرد و سپس توسط متد DrawImage میخواهیم که تصویر مبدا را با مختصات شیء Rectangle از نقطه 0 و 0 بوم آغاز کرده و تا انتهای آن شروع به ترسیم کند. سپس آن را ذخیره و مجددا در قالب همان آرایهای از بایتها بر میگردانیم.
تنها یک نکته را به خاطر داشته باشید که مقادیر مختصاتی که پلاگین جی کوئری ارسال میکند در قالب اعداد اعشاری هستند و برای ارسال و دریافت آنها در سرور این نکته را به خاطر داشته باشید.
using System.Collections.Generic; namespace AutoMapperComparison.Models { public class User { public int Id { get; set; } public string Name { get; set; } public ICollection<Address> Addresses { get; set; } } }
using System.ComponentModel.DataAnnotations.Schema; namespace AutoMapperComparison.Models { public class Address { public int Id { get; set; } public double? Code { get; set; } public string Title { get; set; } public int UserId { get; set; } [ForeignKey(nameof(UserId))] public virtual User User { get; set; } } }
using EntityFramework.BulkInsert.Extensions; using System.Collections.Generic; using System.Data.Entity; using System.Data.SqlClient; namespace AutoMapperComparison.Models { public class AppDbContextInitializer : DropCreateDatabaseAlways<AppDbContext> { protected override void Seed(AppDbContext context) { User user = context.Users.Add(new User { Name = "Test" }); context.SaveChanges(); List<Address> addresses = new List<Address>(); for (int i = 0; i < 500000; i++) { addresses.Add(new Address { Id = i, Code = 1, Title = "Test", UserId = user.Id }); } context.BulkInsert(addresses); base.Seed(context); } } public class AppDbContext : DbContext { static AppDbContext() { Database.SetInitializer(new AppDbContextInitializer()); //Database.SetInitializer<AppDbContext>(null); } public AppDbContext() : base(new SqlConnection(@"Data Source=.;Initial Catalog=AppDbContext;Integrated Security=True"), contextOwnsConnection: true) { Configuration.AutoDetectChangesEnabled = false; Configuration.EnsureTransactionsForFunctionsAndCommands = false; Configuration.LazyLoadingEnabled = false; Configuration.ProxyCreationEnabled = false; Configuration.ValidateOnSaveEnabled = false; Configuration.UseDatabaseNullSemantics = false; } public DbSet<User> Users { get; set; } public DbSet<Address> Addresses { get; set; } } }
namespace AutoMapperComparison.Models { public class AddressDto { public int Id { get; set; } public double? Code { get; set; } public string Title { get; set; } public int UserId { get; set; } public string UserName { get; set; } } }
using AutoMapper; using AutoMapper.QueryableExtensions; using AutoMapperComparison.Models; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace AutoMapperComparison { public class Program { public static void Main() { Mapper.Initialize(cfg => { cfg.CreateMap<Address, AddressDto>(); }); Console.WriteLine($"Create Db {DateTimeOffset.UtcNow}"); using (AppDbContext db = new AppDbContext()) { db.Database.Initialize(force: true); db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); //Removes all clean buffers from the buffer pool, and columnstore objects from the columnstore object pool Console.WriteLine(db.Addresses.ProjectTo<AddressDto>()); Console.WriteLine(db.Addresses.Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name })); } Console.WriteLine($"Normal Select {DateTimeOffset.UtcNow}"); using (AppDbContext db = new AppDbContext()) { db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); Stopwatch watch = Stopwatch.StartNew(); List<AddressDto> addresses = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList(); List<AddressDto> addresses2 = db.Addresses.AsNoTracking().Select(add => new AddressDto { Id = add.Id, Code = add.Code, Title = add.Title, UserId = add.UserId, UserName = add.User.Name }).ToList(); watch.Stop(); Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}"); } Console.WriteLine($"AutoMapper Exec {DateTimeOffset.UtcNow}"); using (AppDbContext db = new AppDbContext()) { db.Database.ExecuteSqlCommand("DBCC DROPCLEANBUFFERS"); Stopwatch watch = Stopwatch.StartNew(); List<AddressDto> addresses = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList(); List<AddressDto> addresses2 = db.Addresses.AsNoTracking().ProjectTo<AddressDto>().ToList(); watch.Stop(); Console.WriteLine($"{watch.ElapsedMilliseconds} {addresses.Count} {addresses2.Count}"); } Console.ReadKey(); } } }
حال نتایج بدست آمده، در قسمت پایینتر آن نمایان میشود:
البته نتیجهی این آزمایش بسته به سخت افزار سیستم شما ممکن است کمی متفاوت باشد.
در سه آزمایش دیگر به صورت متوالی نتیجهی زیر بدست آمد:
Normal | AutoMapper |
2451 | 2378 |
2120 | 2111 |
2202 | 2124 |
اگر این مقدار جزئی از تفاوت بین دو نوع مختلف آزمایش را مورد نظر نگیریم، میتوان گفت که هر دو روش نتیجهی کاملا یکسانی خواهند داشت. فقط با استفاده از AutoMapper کدهای کمتری نوشته شدهاست!
اما دلیل چیست؟ از آنجایی که ProjectTo از Dto به Model انجام شده و Lambda Expressionی که به سمت Entity Framework فرستاده شدهاست با روش Normal کاملا برابر است و بقیهی عملیات توسط EF انجام میشود، با قاطعیت میتوان گفت که هر دو روش ذکر شده از نظر Performance کاملا یکسان خواهند بود.
نکته: البته به این موضوع باید توجه شود که اگر همین آزمایش را بطور مثال با استفاده از یک Listی از رکوردهای درون Memory ساخته شده توسط خودمان انجام دهیم، آن موقع نتیجهی یکسانی نخواهیم داشت، به دلیل اینکه EFی دیگر وجود نخواهد داشت که مسئولیت بازگشت دادهها را بر عهده بگیرد. از آنجائیکه اکثر کارهایی که توقع داریم AutoMapper برای ما انجام دهد، توسط ORM بازگشت داده میشود، پس میتوان گفت نکتهی فوق تقریبا در دنیای واقعی رخ نخواهد داد و باعث مشکل نخواهد شد.
نگاهی به انواع Aspects موجود در کتابخانه PostSharp
- SQL Server هم تجاری است. Windows هم از بنیان تجاری است. احتمالا از هر دوی اینها استفاده میکنید. تجاری بودن دلیلی برای سرکوب اشتیاق به یادگیری مطلبی نیست و نخواهد بود.
- ضمن اینکه در قسمتهای بعدی نمونههای سورس باز هم معرفی شدهاند.