مطالب
ASP.NET MVC #20

تهیه گزارشات تحت وب به کمک WebGrid

WebGrid از ASP.NET MVC 3.0 به صورت توکار به شکل یک Html Helper در دسترس می‌باشد و هدف از آن ساده‌تر سازی تهیه گزارشات تحت وب است. البته این گرید، تنها گرید مهیای مخصوص ASP.NET MVC نیست و پروژه MVC Contrib یا شرکت Telerik نیز نمونه‌های دیگری را ارائه داده‌اند؛ اما از این جهت که این Html Helper، بدون نیاز به کتابخانه‌های جانبی در دسترس است، بررسی آن ضروری می‌باشد.


صورت مساله

لیستی از کارمندان به همراه حقوق ماهیانه آن‌ها در دست است. اکنون نیاز به گزارشی تحت وب، با مشخصات زیر می‌باشد:
1- گزارش باید دارای صفحه بندی بوده و هر صفحه تنها 10 ردیف را نمایش دهد.
2- سطرها باید یک در میان دارای رنگی متفاوت باشند.
3- ستون حقوق کارمندان در پایین هر صفحه، باید دارای جمع باشد.
4- بتوان با کلیک بر روی عنوان هر ستون، اطلاعات را بر اساس ستون انتخابی، مرتب ساخت.
5- لینک‌های حذف یا ویرایش یک ردیف نیز در این گزارش مهیا باشد.
6- لیست تهیه شده، دارای ستونی به نام «ردیف» نیست. این ستون را نیز به صورت خودکار اضافه کنید.
7- لیست نهایی اطلاعات، دارای ستونی به نام مالیات نیست. فقط حقوق کارمندان ذکر شده است. ستون محاسبه شده مالیات نیز باید به صورت خودکار در این گزارش نمایش داده شود. این ستون نیز باید دارای جمع پایین هر صفحه باشد.
8- تمام اعداد این گزارش در حین نمایش باید دارای جدا کننده سه رقمی باشند.
9- تاریخ‌های موجود در لیست، میلادی هستند. نیاز است این تاریخ‌ها در حین نمایش شمسی شوند.
10- انتهای هر صفحه گزارش باید بتوان برچسب «صفحه y/n» را مشاهده کرد. n در اینجا منظور تعداد کل صفحات است و y شماره صفحه جاری می‌باشد.
11- انتهای هر صفحه گزارش باید بتوان برچسب «رکوردهای y تا x از n» را مشاهده کرد. n در اینجا منظور تعداد کل رکوردها است.
12- نام کوچک هر کارمند، ضخیم نمایش داده شود.
13- به ازای هر شماره کارمندی، یک تصویر در پوشه images سایت وجود دارد. برای مثال images/id.jpg. ستونی برای نمایش تصویر متناظر با هر کارمند نیز باید اضافه شود.
14- به ازای هر کارمند، تعدادی پروژه هم وجود دارد. پروژه‌های متناظر را توسط یک گرید تو در تو نمایش دهید.


راه حل به کمک استفاده از WebGrid

ابتدا یک پروژه خالی ASP.NET MVC را آغاز کنید. سپس مدل‌های زیر را به آن اضافه نمائید (یک کارمند که می‌تواند تعداد پروژه منتسب داشته باشد):

using System;
using System.Collections.Generic;

namespace MvcApplication17.Models
{
public class Employee
{
public int Id { set; get; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime AddDate { get; set; }
public double Salary { get; set; }
public IList<Project> Projects { get; set; }
}
}

namespace MvcApplication17.Models
{
public class Project
{
public int Id { set; get; }
public string Name { set; get; }
}
}

سپس منبع داده نمونه زیر را به پروژه اضافه کنید. به عمد از ORM‌ خاصی استفاده نشده تا بتوانید پروژه جاری را به سادگی در یک پروژه آزمایشی جدید،‌ تکرار کنید.
using System;
using System.Collections.Generic;

namespace MvcApplication17.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
var rnd = new Random();
for (int i = 1; i <= 1000; i++)
{
list.Add(new Employee
{
Id = i + 1000,
FirstName = "fName " + i,
LastName = "lName " + i,
AddDate = DateTime.Now.AddYears(-rnd.Next(1, 10)),
Salary = rnd.Next(400, 3000),
Projects = CreateRandomProjects()
});
}
return list;
}

private static IList<Project> CreateRandomProjects()
{
var list = new List<Project>();
var rnd = new Random();
for (int i = 0; i < rnd.Next(1, 7); i++)
{
list.Add(new Project
{
Id = i,
Name = "Project " + i
});
}
return list;
}
}
}


در ادامه یک کنترلر جدید را با محتوای زیر اضافه نمائید:
using System.Web.Mvc;
using MvcApplication17.Models;

namespace MvcApplication17.Controllers
{
public class HomeController : Controller
{
[HttpPost]
public ActionResult Delete(int? id)
{
return RedirectToAction("Index");
}

[HttpGet]
public ActionResult Edit(int? id)
{
return View();
}

[HttpGet]
public ActionResult Index(string sort, string sortdir, int? page = 1)
{
var list = EmployeeDataSource.CreateEmployees();
return View(list);
}
}
}

علت تعریف متد index با پارامترهای sort و غیره به URLهای خودکاری از نوع زیر بر می‌گردد:

http://localhost:3034/?sort=LastName&sortdir=ASC&page=3

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

سپس یک View خالی را نیز برای متد Index ایجاد کنید. تا اینجا تنظیمات اولیه پروژه انجام شد.
کدهای کامل View را در ادامه ملاحظه می‌کنید:

@using System.Globalization
@model IList<MvcApplication17.Models.Employee>

@{
ViewBag.Title = "Index";
}

@helper WebGridPageFirstItem(WebGrid grid)
{
@(((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1));
}

@helper WebGridPageLastItem(WebGrid grid)
{
if (grid.TotalRowCount < (grid.PageIndex + 1 * grid.RowsPerPage))
{
@grid.TotalRowCount;
}
else
{
@((grid.PageIndex + 1) * grid.RowsPerPage);
}
}

<h2>Employees List</h2>

@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}

<div id="container">
@grid.GetHtml(
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
mode: WebGridPagerModes.All,
columns: grid.Columns(
grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");

}),
grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
)
)

<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount

@*
@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
*@
</div>

@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}


توضیحات ریز جزئیات View فوق


تعریف ابتدایی شیء WebGrid و مقدار دهی آن
در ابتدا نیاز است یک وهله از شیء WebGrid را ایجاد کنیم. در اینجا می‌توان تنظیم کرد که آیا نیاز است اطلاعات نمایش داده شده دارای صفحه بندی (canPage) خودکار باشند؟ منبع داده (source) کدام است. در صورت فعال سازی صفحه بندی خودکار، چه تعداد ردیف (rowsPerPage) در هر صفحه نمایش داده شود. آیا نیاز است بتوان با کلیک بر روی سر ستون‌ها، اطلاعات را بر اساس فیلد متناظر با آن مرتب (canSort) ساخت؟ همچنین در صورت نیاز به مرتب سازی، اولین باری که گرید نمایش داده می‌شود، بر اساس چه فیلدی (defaultSort) باید مرتب شده نمایش داده شود:

@{ 
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}

در اینجا همچنین سه متغیر کمکی هم تعریف شده که از این‌ها برای تهیه جمع ستون‌های حقوق و مالیات و همچنین نمایش شماره ردیف جاری استفاده می‌شود. فرمول نحوه محاسبه اولین ردیف هر صفحه را هم ملاحظه می‌کنید. شماره ردیف‌های بعدی، rowIndex++ خواهند بود.


تعریف رنگ و لعاب گرید نمایش داده شده
در ادامه به کمک متد grid.GetHtml، رشته‌ای معادل اطلاعات HTML صفحه جاری، بازگشت داده می‌شود. در اینجا می‌توان یک سری خواص تکمیلی را تنظیم نمود. برای مثال:
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },

هر کدام از این رشته‌ها در حین رندر نهایی گرید،‌ تبدیل به یک class خواهند شد. برای نمونه:

<div id="container">
<table class="webgrid" id="MyGrid">
<thead>
<tr class="webgrid-header">

به این ترتیب با اندکی ویرایش css سایت، می‌توان انواع و اقسام رنگ‌ها را به سطرها و ستون‌های گرید نهایی اعمال کرد. برای مثال اطلاعات زیر را به فایل css سایت اضافه نمائید:

/* Styles for WebGrid
-----------------------------------------------------------*/
.webgrid
{
width: 100%;
margin: 0px;
padding: 0px;
border: 0px;
border-collapse: collapse;
font-family: Tahoma;
font-size: 9pt;
}

.webgrid a
{
color: #000;
}

.webgrid-header
{
padding: 0px 5px;
text-align: center;
border-bottom: 2px solid #739ace;
height: 20px;
border-top: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-header th
{
background-color: #eaf0ff;
border-right: 1px solid #ddd;
}

.webgrid-footer
{
padding: 6px 5px;
text-align: center;
background-color: #e8eef4;
border-top: 2px solid #3966A2;
height: 25px;
border-bottom: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-alternating-row
{
height: 22px;
background-color: #f2f2f2;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-row-style
{
height: 22px;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-selected-row
{
font-weight: bold;
}

.text-align-center-col
{
text-align: center;
}

.total-row
{
background-color:#f9eef4;
}

همانطور که ملاحظه می‌کنید، رنگ‌های ردیف‌ها، هدر و فوتر گرید و غیره در اینجا تنظیم می‌شوند.
به علاوه اگر دقت کرده باشید در تعاریف گرید، htmlAttributes هم مقدار دهی شده است. در اینجا به کمک یک anonymously typed object، مقدار id گرید مشخص شده است. از این id در حین کار با jQuery‌ استفاده خواهیم کرد.


تعیین نوع Pager
پارامتر دیگری که در متد grid.GetHtml تنظیم شده است، mode: WebGridPagerModes.All می‌باشد. WebGridPagerModes یک enum با محتوای زیر است و توسط آن می‌توان نوع Pager گرید را تعیین کرد:

[Flags]
public enum WebGridPagerModes
{
Numeric = 1,
//
NextPrevious = 2,
//
FirstLast = 4,
//
All = 7,
}

نحوه تعریف ستون‌های گرید
اکنون به مهم‌ترین قسمت تهیه گزارش رسیده‌ایم. در اینجا با مقدار دهی پارامتر columns، نحوه نمایش اطلاعات ستون‌های مختلف مشخص می‌گردد. مقداری که باید در اینجا تنظیم شود، آرایه‌ای از نوع WebGridColumn می‌باشد و مرسوم است به کمک متد کمکی grid.Columns،‌ اینکار را انجام داد.
متد کمکی grid.Column، یک وهله از شیء WebGridColumn را بر می‌گرداند و از آن برای تعریف هر ستون استفاده خواهیم کرد. توسط پارامتر columnName آن،‌ نام فیلدی که باید اطلاعات ستون جاری از آن اخذ شود مشخص می‌شود. به کمک پارامتر header،‌ عبارت سرستون متناظر تنظیم می‌گردد. پارامتر format، مهم‌ترین و توانمندترین پارامتر متد grid.Column است:

grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),

پارامتر format، به نحو زیر تعریف شده است:

Func<dynamic, object> format

به این معنا که هر بار پیش از رندر سطر جاری، زمانیکه قرار است سلولی رندر شود، یک شیء dynamic در اختیار شما قرار می‌گیرد. این شیء dynamic یک رکورد از اطلاعات Model جاری است. به این ترتیب به اطلاعات تمام سلول‌های ردیف جاری دسترسی خواهیم داشت. بر این اساس هر نوع پردازشی را که لازم بود، انجام دهید (شبیه به فرمول نویسی در ابزارهای گزارش سازی، اما اینبار با کدهای سی شارپ) و مقدار فرمت شده نهایی را به صورت یک رشته بر گردانید. این رشته نهایتا در سلول جاری درج خواهد شد.
اگر از پارامتر فرمت استفاده نشود، همان مقدار فیلد جاری بدون تغییری رندر می‌گردد.
حداقل به دو نحو می‌توان پارامتر فرمت را مقدار دهی کرد:

format: @<span style='font-weight: bold'>@item.FirstName</span>
or
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
}

مستقیما از توانمندی‌های Razor استفاده کنید. مثلا یک تگ کامل را بدون نیاز به محصور سازی آن بین "" شروع کنید. سپس @item به وهله‌ای از رکورد در دسترس اشاره می‌کند که در اینجا وهله‌ای از شیء کارمند است.
و یا همانند روشی که برای محاسبه جمع حقوق هر صفحه مشاهده می‌کنید، مستقیما از lambda expressions برای تعریف یک anonymous delegate استفاده کنید.


نحوه اضافه کردن ستون ردیف
ستون ردیف، یک ستون محاسبه شده (calculated field) است:

grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),

نیازی نیست حتما یک grid.Column، به فیلدی در کلاس کارمند اشاره کند. مقدار سفارشی آن را به کمک پارامتر format تعیین خواهیم کرد. هر بار که قرار است یک ردیف رندر شود، یکبار این پارامتر فراخوانی خواهد شد. فرمول محاسبه rowIndex ابتدای صفحه را نیز پیشتر ملاحظه نمودید.


نحوه اضافه کردن ستون سفارشی تصاویر کارمندها
ستون تصویر کارمندها نیز مستقیما در کلاس کارمند تعریف نشده است. بنابراین می‌توان آن‌را با مقدار دهی صحیح پارامتر format ایجاد کرد:

grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),


در این مثال، تصاویر کارمندها در پوشه images واقع در ریشه سایت، قرار دارند. به همین جهت از متد Url.Content برای مقدار دهی صحیح آن استفاده کردیم. به علاوه در اینجا @item.Id به Id رکورد در حال رندر اشاره می‌کند.


نحوه تبدیل تاریخ‌ها به تاریخ شمسی
در ادامه بازهم به کمک پارامتر format، یک وهله از شیء dynamic اشاره کننده به رکورد در حال رندر را دریافت می‌کنیم. سپس فرصت خواهیم داشت تا بر این اساس، فرمول نویسی کنیم. دست آخر هم رشته مورد نظر نهایی را بازگشت می‌دهیم:

grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),


اضافه کردن ستون سفارشی مالیات
در کلاس کارمند، خاصیت حقوق وجود دارد اما مالیات خیر. با توجه به آن می‌توانیم به کمک پارامتر format، به اطلاعات شیء dynamic در حال رندر دسترسی داشته باشیم. بنابراین به اطلاعات حقوق دسترسی داریم و سپس با کمی فرمول نویسی، مقدار نهایی مورد نظر را بازگشت خواهیم داد. همچنین در اینجا می‌توان نحوه بازگشت مقدار حقوق را به صورت رشته‌ای حاوی جدا کننده‌های سه رقمی نیز مشاهده کرد:

grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),


اضافه کردن گردید‌های تو در تو
متد Grid.GetHtml، یک رشته را بر می‌گرداند. بنابراین در هر چند سطح که نیاز باشد می‌توان یک گرید را بر اساس اطلاعات دردسترس رندر کرد و سپس بازگشت داد:

grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),


در اینجا کار اصلی از طریق پارامتر format شروع می‌شود. سپس به کمک item.Projects به لیست پروژه‌های هر کارمند دسترسی خواهیم داشت. بر این اساس یک گرید جدید را تولید کرد و سپس رشته معادل با آن را به کمک متد subGrid.GetHtml دریافت و بازگشت می‌دهیم. این رشته در سلول جاری درج خواهد شد. به نوعی یک گزارش master detail یا sub report را تولید کرده‌ایم.


اضافه کردن دکمه‌های ویرایش، حذف و انتخاب
هر سه دکمه ویرایش، حذف و انتخاب در ستون‌هایی سفارشی قرار خواهند گرفت. بنابراین مقدار دهی header و format متد grid.Column کفایت می‌کند:

grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))


نکته جدیدی که در اینجا وجود دارد متد item.GetSelectLink می‌باشد. این متد جزو متدهای توکار گرید است و کار آن بازگشت دادن شیء grid.SelectedRow می‌باشد. این شیء پویا، حاوی اطلاعات رکورد انتخاب شده است. برای مثال اگر نیاز باشد این اطلاعات به صفحه‌ای ارسال شود، می‌توان از روش زیر استفاده کرد:

@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}


نمایش برچسب‌های صفحه x از n و رکوردهای x تا y از z
در یک گزارش خوب باید مشخص باشد که صفحه جاری، کدامین صفحه از چه تعداد صفحه کلی است. یا رکوردهای صفحه جاری چه بازه‌ای از تعداد رکوردهای کلی را تشکیل می‌دهند. برای این منظور چند متد کمکی به نام‌های WebGridPageFirstItem و WebGridPageLastItem تهیه شده‌اند که آن‌ها را در ابتدای View ارائه شده، مشاهده نمودید:

<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount, 
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount

نمایش جمع ستون‌های حقوق و مالیات در هر صفحه
گرید توکار همراه با ASP.NET MVC در این مورد راه حلی را ارائه نمی‌دهد. بنابراین باید اندکی دست به ابتکار زد. مثلا:

@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}

در این مثال به کمک jQuery با توجه به اینکه id گرید ما MyGrid است، یک ردیف سفارشی که همان جمع محاسبه شده است، به tbody جدول نهایی تولیدی اضافه می‌شود. از tbody:first هم در اینجا استفاده شده است تا ردیف اضافه شده به گریدهای تو در تو اعمال نشود.
سپس فایل Views\Shared\_Layout.cshtml را گشوده و از section تعریف شده، برای مقدار دهی master page سایت، استفاده نمائید:

<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
@RenderSection("script", required: false)
</head>

مطالب
استفاده از نگارش سوم Google Analytics API در سرویس‌های ویندوز یا برنامه‌های وب
در زمان نگارش این مطلب، آخرین نگارش API مخصوص Google Analytics، نگارش سوم آن است و ... کار کردن با آن دارای مراحل خاصی است که حتما باید رعایت شوند. در غیر اینصورت عملا در یک برنامه‌ی وب یا سرویس ویندوز قابل اجرا نخواهند بود. زیرا در حالت متداول کار با API مخصوص Google Analytics، ابتدا یک صفحه‌ی لاگین به Gmail باز می‌شود که باید به صورت اجباری، مراحل آن را انجام داد تا مشخصات تائید شده‌ی اکانت در حال استفاده‌ی از API، در پوشه‌ی AppData ویندوز برای استفاده‌های بعدی ذخیره شود. این مورد برای یک برنامه‌ی دسکتاپ معمولی مشکل ساز نیست؛ زیرا کاربر برنامه، به سادگی می‌تواند صفحه‌ی مرورگری را که باز شده‌است، دنبال کرده و به اکانت گوگل خود وارد شود. اما این مراحل را نمی‌توان در یک برنامه‌ی وب یا سرویس ویندوز پیگیری کرد، زیرا عموما امکان لاگین از راه دور به سرور و مدیریت صفحه‌ی لاگین به Gmail وجود ندارد یا بهتر است عنوان شود، بی‌معنا است. برای حل این مشکل، گوگل راه حل دیگری را تحت عنوان اکانت‌های سرویس، ارائه داده است که پس از ایجاد آن، یک یک فایل X509 Certificate برای اعتبارسنجی سرویس، در اختیار برنامه نویس قرار می‌گیرد تا بدون نیاز به لاگین دستی به Gmail، بتواند از API گوگل استفاده کند. در ادامه نحوه‌ی فعال سازی این قابلیت و استفاده از آن‌را بررسی خواهیم کرد.


ثبت برنامه‌ی خود در گوگل و انجام تنظیمات آن

اولین کاری که برای استفاده از نگارش سوم Google Analytics API باید صورت گیرد، ثبت برنامه‌ی خود در Google Developer Console است. برای این منظور ابتدا به آدرس ذیل وارد شوید:
سپس بر روی دکمه‌ی Create project کلیک کنید. نام دلخواهی را وارد کرده و در ادامه بر روی دکمه‌ی Create کلیک نمائید تا پروفایل این پروژه ایجاد شود.



تنها نکته‌ی مهم این قسمت، بخاطر سپردن نام پروژه است. زیرا از آن جهت اتصال به API گوگل استفاده خواهد شد.
پس از ایجاد پروژه، به صفحه‌ی آن وارد شوید و از منوی سمت چپ صفحه، گزینه‌ی Credentials را انتخاب کنید. در ادامه در صفحه‌ی باز شده، بر روی دکمه‌ی Create new client id کلیک نمائید.


در صفحه‌ی باز شده، گزینه‌ی Service account را انتخاب کنید. اگر سایر گزینه‌ها را انتخاب نمائید، کاربری که قرار است از API استفاده کند، باید بتواند توسط مرورگر نصب شده‌ی بر روی کامپیوتر اجرا کننده‌ی برنامه، یکبار به گوگل لاگین نماید که این مورد مطلوب برنامه‌های وب و همچنین سرویس‌ها نیست.


در اینجا ابتدا یک فایل مجوز p12 را به صورت خودکار دریافت خواهید کرد و همچنین پس از ایجاد client id، نیاز است، ایمیل آن‌را جایی یادداشت نمائید:


از این ایمیل و همچنین فایل p12 ارائه شده، جهت لاگین به سرور استفاده خواهد شد.
همچنین نیاز است تا به برگه‌ی APIs پروژه‌ی ایجاد شده رجوع کرد و گزینه‌ی Analytics API آن‌را فعال نمود:


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


دادن دسترسی به Client ID ثبت شده در برنامه‌ی Google Analytics

پس از اینکه Client ID سرویس خود را ثبت کردید، نیاز است به اکانت Google Analytics خود وارد شوید. سپس در منوی آن، گزینه‌ی Admin را پیدا کرده و به آن قسمت، وارد شوید:


در ادامه به گزینه‌ی User management آن وارد شده و به ایمیل Client ID ایجاد شده در قسمت قبل، دسترسی خواندن و آنالیز را اعطاء کنید:


در صورت عدم رعایت این مساله، کلاینت API، قادر به دسترسی به Google Analytics نخواهد بود.


استفاده از نگارش سوم Google Analytics API در دات نت

قسمت مهم کار، تنظیمات فوق است که در صورت عدم رعایت آن‌ها، شاید نصف روزی را مشغول به دیباگ برنامه شوید. در ادامه نیاز است پیشنیازهای دسترسی به نگارش سوم Google Analytics API را نصب کنیم. برای این منظور، سه بسته‌ی نیوگت ذیل را توسط کنسول پاورشل نیوگت، به برنامه اضافه کنید:
 PM> Install-Package Google.Apis
PM> Install-Package Google.Apis.auth
PM> Install-Package Google.Apis.Analytics.v3
پس از نصب، کلاس GoogleAnalyticsApiV3 زیر، جزئیات دسترسی به Google Analytics API را کپسوله می‌کند:
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Google.Apis.Analytics.v3;
using Google.Apis.Analytics.v3.Data;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;

namespace GoogleAnalyticsAPIv3Tests
{
    public class AnalyticsQueryParameters
    {
        public DateTime Start { set; get; }
        public DateTime End { set; get; } 
        public string Dimensions { set; get; } 
        public string Filters { set; get; }
        public string Metrics { set; get; }
    }

    public class AnalyticsAuthentication
    {
        public Uri SiteUrl { set; get; }
        public string ApplicationName { set; get; }
        public string ServiceAccountEmail { set; get; }
        public string KeyFilePath { set; get; }
        public string KeyFilePassword { set; get; }

        public AnalyticsAuthentication()
        {
            KeyFilePassword = "notasecret";
        }
    }

    public class GoogleAnalyticsApiV3
    {
        public AnalyticsAuthentication Authentication { set; get; }
        public AnalyticsQueryParameters QueryParameters { set; get; }

        public GaData GetData()
        {
            var service = createAnalyticsService();
            var profile = getProfile(service);
            var query = service.Data.Ga.Get("ga:" + profile.Id,
                                QueryParameters.Start.ToString("yyyy-MM-dd"),
                                QueryParameters.End.ToString("yyyy-MM-dd"),
                                QueryParameters.Metrics);
            query.Dimensions = QueryParameters.Dimensions;
            query.Filters = QueryParameters.Filters;
            query.SamplingLevel = DataResource.GaResource.GetRequest.SamplingLevelEnum.HIGHERPRECISION;
            return query.Execute();
        }

        private AnalyticsService createAnalyticsService()
        {
            var certificate = new X509Certificate2(Authentication.KeyFilePath, Authentication.KeyFilePassword, X509KeyStorageFlags.Exportable);
            var credential = new ServiceAccountCredential(
                new ServiceAccountCredential.Initializer(Authentication.ServiceAccountEmail)
                {
                    Scopes = new[] { AnalyticsService.Scope.AnalyticsReadonly }
                }.FromCertificate(certificate));

            return new AnalyticsService(new BaseClientService.Initializer
            {
                HttpClientInitializer = credential,
                ApplicationName = Authentication.ApplicationName
            });
        }

        private Profile getProfile(AnalyticsService service)
        {
            var accountListRequest = service.Management.Accounts.List();
            var accountList = accountListRequest.Execute();
            var site = Authentication.SiteUrl.Host.ToLowerInvariant();
            var account = accountList.Items.FirstOrDefault(x => x.Name.ToLowerInvariant().Contains(site));
            var webPropertyListRequest = service.Management.Webproperties.List(account.Id);
            var webPropertyList = webPropertyListRequest.Execute();
            var sitePropertyList = webPropertyList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site));
            var profileListRequest = service.Management.Profiles.List(account.Id, sitePropertyList.Id);
            var profileList = profileListRequest.Execute();
            return profileList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site));
        }
    }
}
در اینجا در ابتدا بر اساس فایل p12 ایی که از گوگل دریافت شد، یک X509Certificate2 ایجاد می‌شود. پسورد این فایل مساوی است با ثابت notasecret که در همان زمان تولید اکانت سرویس در گوگل، لحظه‌ای در صفحه نمایش داده خواهد شد. به کمک آن و همچنین ServiceAccountEmail ایمیلی که پیشتر به آن اشاره شد، می‌توان به AnalyticsService لاگین کرد. به این ترتیب به صورت خودکار می‌توان شماره پروفایل اکانت سایت خود را یافت و از آن در حین فراخوانی service.Data.Ga.Get استفاده کرد.


مثالی از نحوه استفاده از کلاس GoogleAnalyticsApiV3

در ادامه یک برنامه‌ی کنسول را ملاحظه می‌کنید که از کلاس GoogleAnalyticsApiV3 استفاده می‌کند:
using System;
using System.Collections.Generic;
using System.Linq;

namespace GoogleAnalyticsAPIv3Tests
{
    class Program
    {

        static void Main(string[] args)
        {
            var statistics = new GoogleAnalyticsApiV3
            {
                Authentication = new AnalyticsAuthentication
                {
                    ApplicationName = "My Project",
                    KeyFilePath = "811e1d9976cd516b55-privatekey.p12",
                    ServiceAccountEmail = "10152bng4j3mq@developer.gserviceaccount.com",
                    SiteUrl = new Uri("https://www.dntips.ir/")
                },
                QueryParameters = new AnalyticsQueryParameters
                {
                    Start = DateTime.Now.AddDays(-7),
                    End = DateTime.Now,
                    Dimensions = "ga:date",
                    Filters = null,
                    Metrics = "ga:users,ga:sessions,ga:pageviews"
                }
            }.GetData();


            foreach (var result in statistics.TotalsForAllResults)
            {
                Console.WriteLine(result.Key + " -> total:" + result.Value);
            }
            Console.WriteLine();

            foreach (var row in statistics.ColumnHeaders)
            {
                Console.Write(row.Name + "\t");
            }
            Console.WriteLine();

            foreach (var row in statistics.Rows)
            {
                var rowItems = (List<string>)row;
                Console.WriteLine(rowItems.Aggregate((s1, s2) => s1 + "\t" + s2));
            }

            Console.ReadLine();
        }
    }
}

چند نکته
ApplicationName همان نام پروژه‌ای است که ابتدای کار، در گوگل ایجاد کردیم.
KeyFilePath مسیر فایل مجوز p12 ایی است که گوگل در حین ایجاد اکانت سرویس، در اختیار ما قرار می‌دهد.
ServiceAccountEmail آدرس ایمیل اکانت سرویس است که در قسمت ادمین Google Analytics به آن دسترسی دادیم.
SiteUrl آدرس سایت شما است که هم اکنون در Google Analytics دارای یک اکانت و پروفایل ثبت شده‌است.
توسط AnalyticsQueryParameters می‌توان نحوه‌ی کوئری گرفتن از Google Analytics را مشخص کرد. تاریخ شروع و پایان گزارش گیری در آن مشخص هستند. در مورد پارامترهایی مانند Dimensions و Metrics بهتر است به مرجع کامل آن در گوگل مراجعه نمائید:
Dimensions & Metrics Reference

برای نمونه در مثال فوق، تعداد کاربران، سشن‌های آن و همچنین تعداد بار مشاهده‌ی صفحات، گزارشگیری می‌شود.


برای مطالعه بیشتر
Using Google APIs in Windows Store Apps
How To Use Google Analytics From C# With OAuth
Google Analytic’s API v3 with C#
.NET Library for Accessing and Querying Google Analytics V3 via Service Account
Google OAuth2 C#
مطالب
تنظیمات کش توزیع شده‌ی مبتنی بر SQL Server در ASP.NET Core
ASP.NET Core به همراه زیر ساختی‌است جهت خارج کردن امکانات Caching درون حافظه‌ای آن از سرور جاری و انتقال آن به سرورهای دیگر جهت کاهش بار سرور و برنامه. این کش توزیع شده را می‌توان به عنوان زیرساختی برای مدیریت سشن‌ها، مدیریت اطلاعات کش و همچنین مدیریت کوکی‌های حجیم ASP.NET Core Identity نیز بکار گرفت. برای مثال بجای ارسال یک کوکی حجیم بالای 5 کیلوبایت به کلاینت، فقط ID رمزنگاری شده‌ی آن‌را ارسال کرد و اصل کوکی را در داخل دیتابیس ذخیره و بازیابی نمود. این مساله هم مقیاس پذیری برنامه را افزایش خواهد داد و هم امنیت آن‌را با عدم ارسال اصل محتوای کوکی به سمت کلاینت‌ها و یا ذخیره سازی اطلاعات سشن‌ها در بانک اطلاعاتی، مشکلات راه اندازی مجدد برنامه را به طور کامل برطرف می‌کنند و در این حالت بازیابی Application pool و یا کرش برنامه و یا ری استارت شدن کل سرور، سبب از بین رفتن سشن‌های کاربران نخواهند شد. بنابراین آشنایی با نحوه‌ی راه اندازی این امکانات، حداقل از بعد امنیتی بسیار مفید هستند؛ حتی اگر سرور ذخیره کننده‌ی این اطلاعات، همان سرور و بانک اطلاعاتی اصلی برنامه باشند.


پیشنیازهای کار با کش توزیع شده‌ی مبتنی بر SQL Server

برای کار با کش توزیع شده‌ی با قابلیت ذخیره سازی در یک بانک اطلاعاتی SQL Server، نیاز است دو بسته‌ی ذیل را به فایل project.json برنامه اضافه کرد:
{
    "dependencies": {
        "Microsoft.Extensions.Caching.SqlServer": "1.1.0"
    },
    "tools": {
        "Microsoft.Extensions.Caching.SqlConfig.Tools": "1.1.0-preview4-final"
    }
}
وابستگی که در قسمت dependencies ذکر شده‌است، کلاس‌های اصلی کار با کش توزیع شده را به برنامه اضافه می‌کند. ذکر وابستگی قسمت tools، اختیاری است و کار آن، ایجاد جدول مورد نیاز برای ذخیره سازی اطلاعات، در یک بانک اطلاعاتی SQL Server می‌باشد.


ایجاد جدول ذخیره سازی اطلاعات کش توزیع شده به کمک ابزار sql-cache

پس از افزودن و بازیابی ارجاعات فوق، با استفاده از خط فرمان، به پوشه‌ی جاری برنامه وارد شده و دستور ذیل را صادر کنید:
 dotnet sql-cache create "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=sql_cache;Integrated Security=True;" "dbo" "AppSqlCache"
توضیحات:
- در اینجا می‌توان هر نوع رشته‌ی اتصالی معتبری را به انواع و اقسام بانک‌های SQL Server ذکر کرد. برای نمونه در مثال فوق این رشته‌ی اتصالی به یک بانک اطلاعاتی از پیش ایجاد شده‌ی LocalDB اشاره می‌کند. نام دلخواه این بانک اطلاعاتی در اینجا sql_cache ذکر گردیده و نام دلخواه جدولی که قرار است این اطلاعات را ثبت کند AppSqlCache تنظیم شده‌است و dbo، نام اسکیمای جدول است:


در اینجا تصویر ساختار جدولی را که توسط ابزار dotnet sql-cache ایجاد شده‌است، مشاهده می‌کنید. اگر خواستید این جدول را خودتان دستی ایجاد کنید، یک چنین کوئری را باید بر روی دیتابیس مدنظرتان اجرا نمائید:
CREATE TABLE AppSqlCache (
    Id                         NVARCHAR (449)  COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL,
    Value                      VARBINARY (MAX) NOT NULL,
    ExpiresAtTime              DATETIMEOFFSET  NOT NULL,
    SlidingExpirationInSeconds BIGINT          NULL,
    AbsoluteExpiration         DATETIMEOFFSET  NULL,
    CONSTRAINT pk_Id PRIMARY KEY (Id)
);

CREATE NONCLUSTERED INDEX Index_ExpiresAtTime
    ON AppSqlCache(ExpiresAtTime);


ایجاد جدول ذخیره سازی اطلاعات کش توزیع شده به کمک ابزار Migrations در EF Core

زیر ساخت کش توزیع شده‌ی مبتنی بر SQL Server هیچگونه وابستگی به EF Core ندارد و تمام اجزای آن توسط Async ADO.NET نوشته شده‌اند. اما اگر خواستید قسمت ایجاد جدول مورد نیاز آن‌را به ابزار Migrations در EF Core واگذار کنید، روش کار به صورت زیر است:
- ابتدا یک کلاس دلخواه جدید را با محتوای ذیل ایجاد کنید:
    public class AppSqlCache
    {
        public string Id { get; set; }
        public byte[] Value { get; set; }
        public DateTimeOffset ExpiresAtTime { get; set; }
        public long? SlidingExpirationInSeconds { get; set; }
        public DateTimeOffset? AbsoluteExpiration { get; set; }
    }
- سپس تنظیمات ایجاد جدول متناظر با آن را به نحو ذیل تنظیم نمائید:
    public class MyDBDataContext : DbContext
    {
        public virtual DbSet<AppSqlCache> AppSqlCache { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<AppSqlCache>(entity =>
            {
                entity.ToTable(name: "AppSqlCache", schema: "dbo");
                entity.HasIndex(e => e.ExpiresAtTime).HasName("Index_ExpiresAtTime");
                entity.Property(e => e.Id).HasMaxLength(449);
                entity.Property(e => e.Value).IsRequired();
            });
        }
    }
به این ترتیب این جدول جدید به صورت خودکار در کنار سایر جداول برنامه ایجاد خواهند شد.
البته این مورد به شرطی است که بخواهید از یک دیتابیس، هم برای برنامه و هم برای ذخیره سازی اطلاعات کش استفاده کنید.


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

پس از ایجاد جدول مورد نیاز جهت ذخیره سازی اطلاعات کش، اکنون نیاز است این اطلاعات را به برنامه معرفی کرد. برای این منظور به کلاس آغازین برنامه مراجعه کرده و متد الحاقی AddDistributedSqlServerCache را بر روی مجموعه‌ی سرویس‌های موجود فراخوانی کنید؛ تا سرویس‌های این کش توزیع شده نیز به برنامه معرفی شوند:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedSqlServerCache(options =>
    {
        options.ConnectionString = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=sql_cache;Integrated Security=True;";
        options.SchemaName = "dbo";
        options.TableName = "AppSqlCache";
    });
باتوجه به توزیع شده بودن این کش، هیچ الزامی ندارد که ConnectionString ذکر شده‌ی در اینجا با رشته‌ی اتصالی مورد استفاده‌ی توسط EF Core یکی باشد (هرچند مشکلی هم ندارد).


آزمایش کش توزیع شده‌ی تنظیمی با فعال سازی سشن‌ها

سشن‌ها را همانند نکات ذکر شده‌ی در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 16 - کار با Sessions» فعال کنید و سپس مقداری را در آن بنویسید:
public IActionResult Index()
{
   HttpContext.Session.SetString("User", "VahidN");
   return Json(true);
}

public IActionResult About()
{
   var userContent = HttpContext.Session.GetString("User");
   return Json(userContent);
}
اکنون از جدول AppSqlCache کوئری بگیرید:


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


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

همانطور که در مقدمه‌ی بحث نیز عنوان شد، استفاده‌ی از زیر ساخت کش توزیع شده منحصر به استفاده‌ی از آن جهت ذخیره سازی اطلاعات سشن‌ها نیست و از آن می‌توان جهت انواع و اقسام سناریوهای مختلف مورد نیاز استفاده کرد. در این حالت روش دسترسی به این زیر ساخت، از طریق اینترفیس IDistributedCache است. زمانیکه متد AddDistributedSqlServerCache را فراخوانی می‌کنیم، در حقیقت کار ثبت یک چنین سرویسی به صورت خودکار انجام خواهد شد:
 services.Add(ServiceDescriptor.Singleton<IDistributedCache, SqlServerCache>());
به عبارتی کلاس SqlServerCache به صورت singleton به مجموعه‌ی سرویس‌های برنامه اضافه شده‌است و برای دسترسی به آن تنها کافی است اینترفیس IDistributedCache را به کنترلرها و یا سرویس‌های برنامه تزریق و از امکانات آن استفاده کنیم.

در اینجا یک نمونه از این تزریق وابستگی و سپس استفاده‌ی از متدهای Set و Get اینترفیس IDistributedCache را مشاهده می‌کنید:
using System;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class CacheTestController : Controller
    {
        readonly IDistributedCache _cache;
        public CacheTestController(IDistributedCache cache)
        {
            _cache = cache;
        }
 
        public IActionResult SetCacheData()
        {
            var time = DateTime.Now.ToLocalTime().ToString();
            var cacheOptions = new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = DateTime.Now.AddYears(1)
 
            };
            _cache.Set("Time", Encoding.UTF8.GetBytes(time), cacheOptions);
            return View();
        }
 
        public IActionResult GetCacheData()
        {
            var time = Encoding.UTF8.GetString(_cache.Get("Time"));
            ViewBag.data = time;
            return View();
        }
 
        public bool RemoveCacheData()
        {
            _cache.Remove("Time");
            return true;
        }
    }
}
در ابتدای بحث که ساختار جدول ذخیره سازی اطلاعات کش را بررسی کردیم، فیلد value آن یک چنین نوعی را دارد:
  Value  VARBINARY (MAX) NOT NULL,
که در سمت کدهای دات نتی، به شکل آرایه‌ای از بایت‌ها قابل بیان است.
  public byte[] Value { get; set; }
به همین جهت متد Set آن مقدار مدنظر را به صورت آرایه‌ای از بایت‌ها قبول می‌کند.
در این حالت اگر برنامه را اجرا و مسیر http://localhost:7742/CacheTest/SetCacheData را فراخوانی کنیم، اطلاعات ذخیره شده‌ی با کلید Test را می‌توان در بانک اطلاعاتی مشاهده کرد:



Tag helper مخصوص کش توزیع شده

در ASP.NET Core، می‌توان از یک Tag Helper جدید به نام distributed-cache برای کش سمت سرور توزیع شده‌ی محتوای قسمتی از یک View به نحو ذیل استفاده کرد:
<distributed-cache name="MyCacheItem2" expires-sliding="TimeSpan.FromMinutes(30)">
    <p>From distributed-cache</p>
    @DateTime.Now.ToString()
</distributed-cache>
که اطلاعات آن در بانک اطلاعاتی به نحو ذیل ذخیره می‌شود:


در اینجا name به صورت هش شده به صورت کلید کش مورد استفاده قرار می‌گیرد. سپس محتوای تگ distributed-cache رندر شده، تبدیل به آرایه‌ای از بایت‌ها گردیده و در بانک اطلاعاتی ذخیره می‌گردد.
ذکر name در اینجا اجباری است و باید دقت داشت که چون به عنوان کلید بازیابی کش مورد استفاده قرار خواهد گرفت، نباید به اشتباه در قسمت‌های دیگر برنامه با همین نام وارد شود. در غیر اینصورت دو قسمتی که name یکسانی داشته باشند، یک محتوا را نمایش خواهند داد.
اشتراک‌ها
مقایسه کارآیی بین C# Wasm AOT ،C# Wasm Interpreted ،C# Runtime و JavaScript

The conclusion of the analysis:
C# Wasm AOT still has a long way to become a general and performant client side web programming platform. Note: The usage of the Uno.Wasm.Bootstrap toolchain may have affected the performance of some of the benchmarks. Thus, this analysis should not be regarded as a benchmark of the finalized product. However, note that the Uno platform is using ".NET 6 WebAssembly Mixed mode AOT/Interpreter" (source). 

مقایسه کارآیی بین C# Wasm AOT ،C# Wasm Interpreted ،C# Runtime و JavaScript
اشتراک‌ها
کتاب رایگان T4 Succinctly
Familiarity with code generation tools is key for modern software engineers, and whether you know it or not, you’re probably already using one. In T4 Succinctly by Nick Harrison, you’ll learn what goes on behind the scenes during code generation with T4, Visual Studio’s built-in code generator. Then, use those principles to apply metadata to a template and transform it into source code in a variety of languages. 
کتاب رایگان T4 Succinctly
مطالب
آشنایی با قابلیت FileStream اس کیوال سرور 2008 - قسمت سوم

در انتهای قسمت قبل، نحوه‌ی ایجاد یک جدول جدید با فیلدی از نوع فایل استریم بررسی شد، حال اگر جدولی از پیش وجود داشت، نحوه‌ی افزودن فیلد ویژه مورد نظر به آن، به صورت زیر است:

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))
)
در اینجا سعی کرده‌ایم یک رشته ساده را در فیلدی از نوع فایل استریم ذخیره کنیم که روش کار به صورت فوق است. از آنجائیکه مقدار پیش فرض FileId را هنگام تعریف جدول به NEWID تنظیم کرده‌ایم، نیازی به ذکر آن نیست و به صورت خودکار محاسبه و ذخیره خواهد شد.
اگر کنجکاو باشید که این فایل اکنون کجا ذخیره شده و نحوه‌ی مدیریت آن توسط اس کیوال سرور به چه صورتی است، فقط کافی است به مسیری که هنگام افزودن گروه فایل‌ها و فایل مربوطه در تنظیمات خواص دیتابیس در قسمت قبل مشخص کردیم، مراجعه کرد (شکل زیر).



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

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
در رویه ذخیره شده AddFile ، ابتدا رکوردی بر اساس عنوان دلخواه ورودی با یک فایل خالی ایجاد می‌شود. سپس مسیر سیستمی این فایل را در آرگومان خروجی filepath قرار می‌دهیم. SystemFile.PathName از اس کیوال سرور 2008 جهت فیلدهای فایل استریم به اس کیوال سرور اضافه شده است. از این مسیر در برنامه خود جهت نوشتن بایت‌های فایل مورد نظر در آن توسط شیء SqlFileStream استفاده خواهیم کرد.
رویه ذخیره شده 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

اشتراک‌ها
Aurelia-Toolbelt مجموعه کامپوننتی بر اساس Bootstrap 4 برای Aurelia

Aurelia is one of the best frameworks that we have ever seen in terms of software design, hence, we decided to write a bunch of tools for its developers to pave the way for further usage.

aurelia-toolbelt is that, in which we tried to gather the best libraries in Javascript world together in aurelia fashion. Writing custom-elements, value-converters, and so on. We tried not to invent the wheel, so most of the work is a wrapper, or bridge( am not sure whether the way that we coded can be called bridge or not), around other libraries.

  • Its is utterly important for us to provide a link as a reference to the libraries used, or inspired from, so that other developers can visit their product and decide on their own to use which, besides it's one way that we can respect the time and effort of those programmers.

  • All libraries used in aurelia-toolbelt are open-source and free of charge; most of which are  MIT , however we will provide definition whenever it differs. 

GitHub aurelia-toolbelt

NPM aurelia-toolbelt

Twitter aureliatoolbelt

Aurelia-Toolbelt مجموعه کامپوننتی بر اساس Bootstrap 4 برای Aurelia
اشتراک‌ها
مقدمه ای بر Free Style

With the release of Free Style 1.0, I figure it's about time to write about Free Style - how it works, why you'd want to use it and little introduction to CSS-in-JS. This has been a long time coming, with my first commit to Free Style over a year ago, and the first commit to Free Style in its current form 10 months ago. This is not a blog post designed to sway decisions - as always, you should use your own fair judgement. 

مقدمه ای بر Free Style
اشتراک‌ها
کنفرانس Build 2017 بررسی Angular و TypeScript
 In this session, learn why the Angular team uses Typescript to develop at scale inside Google, and how Typescript and Angular work together to provide a powerful platform for your developers and great experiences for your users.  
کنفرانس Build 2017  بررسی Angular و TypeScript
مطالب
تحلیل و بررسی ده روش آسیب پذیری نرم افزار بر اساس متدولوژی 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 استفاده کنید؛ با در نظر داشتن نکات لازم