مطالب
Asp.Net Identity #2
پیشتر در اینجا در مورد تاریخچه‌ی سیستم Identity مطالبی را عنوان کردیم. در این مقاله می‌خواهیم نحوه‌ی برپایی سیستم Identity را بحث کنیم.
ASP.NET Identity مانند ASP.NET Membership به اسکیمای SQL Server وابسته نیست؛ اما Relational Storage همچنان واحد ذخیره سازی پیش فرض و آسانی می‌باشد. بدین جهت که تقریبا بین همه‌ی توسعه دهندگان جا افتاده است. ما در این نوشتار از LocalDB جهت ذخیره سازی جداول استفاده می‌کنیم. ذکر این نکته ضروری است که سیستم Identity از Entity Framework Code First جهت ساخت اسکیما استفاده می‌کند.
پیش از هر چیز، ابتدا یک پروژه‌ی وب را ایجاد کنید؛ مانند شکل زیر:

در مرحله‌ی بعد سه پکیج زیر را باید نصب کنیم :

- Microsoft.AspNet.Identity.EntityFramework

-   Microsoft.AspNet.Identity.OWIN

-  Microsoft.Owin.Host.SystemWeb

بعد از اینکه پکیج‌های بالا را نصب کردیم، باید فایل Web.config را بروز کنیم. اولین مورد، تعریف یک Connection String می‌باشد:
<connectionStrings>
 <add name="IdentityDb" 
      providerName="System.Data.SqlClient"
      connectionString="Data Source=(localdb)\v11.0;Initial Catalog=IdentityDb;Integrated Security=True;Connect Timeout=15;Encrypt=False;TrustServerCertificate=False;MultipleActiveResultSets=True"/>
 </connectionStrings>
بعد از آن، تعریف یک کلید در قسمت AppSettings تحت عنوان Owin:AppStartup است:
<appSettings>
 <add key="webpages:Version" value="3.0.0.0" />
 <add key="webpages:Enabled" value="false" />
 <add key="ClientValidationEnabled" value="true" />
 <add key="UnobtrusiveJavaScriptEnabled" value="true" />
 <add key="owin:AppStartup" value="Users.IdentityConfig" />
 </appSettings>
Owin مدل شروع برنامه (Application Startup Model) خودش را تعریف می‌کند که از کلاس کلی برنامه (منظور Global.asax) جداست. AppSetting تعریف شده با نام owin:Startup شناخته می‌شود و مشخص کننده کلاسی است که Owin وهله سازی خواهد کرد. وقتی برنامه شروع به کار می‌کند، تنظیمات خودش را از این کلاس دریافت خواهد کرد (در این نوشتار کلاس IdentityConfig می‌باشد).

ساخت کلاس User
مرحله‌ی بعد ساخت کلاس User می‌باشد. این کلاس بیانگر یک کاربر می‌باشد. کلاس User باید از کلاس IdentityUser ارث بری کند که این کلاس در فضای نام Microsoft.AspNet.Identity.EntityFramework قرار دارد. کلاس IdentityUser فراهم کننده‌ی یک کاربر عمومی و ابتدایی است. اگر بخواهیم اطلاعات اضافی مربوط به کاربر را ذخیره کنیم باید آنها در کلاسی که از کلاس IdentityUser ارث بری می‌کند قرار دهیم. کلاس ما در اینجا AppUser نام دارد.
using System;
using Microsoft.AspNet.Identity.EntityFramework;
namespace Users.Models 
{
   public class AppUser : IdentityUser 
  {
      // پروپرتی‌های اضافی در اینجا
  }
}

ساخت کلاس Database Context برنامه
مرحله‌ی بعد ساخت کلاس DbContext برنامه می‌باشد که بر روی کلاس AppUser عمل میکند. کلاس Context برنامه که ما در اینجا آن را AppIdentityDbContext تعریف کرده‌ایم، از کلاس <IdentityDbContext<T ارث بری می‌کند که T همان کلاس User می‌باشد (در مثال ما AppUser) .
using System.Data.Entity;
using Microsoft.AspNet.Identity.EntityFramework;
using Users.Models;
namespace Users.Infrastructure {
 public class AppIdentityDbContext : IdentityDbContext<AppUser> 
{
   public AppIdentityDbContext() 
              : base("IdentityDb") { }

  static AppIdentityDbContext() 
 {
    Database.SetInitializer<AppIdentityDbContext>(new IdentityDbInit());
  }
 public static AppIdentityDbContext Create() {
 return new AppIdentityDbContext();
 }
 }
public class IdentityDbInit
 : DropCreateDatabaseIfModelChanges<AppIdentityDbContext> {
 protected override void Seed(AppIdentityDbContext context) {
 PerformInitialSetup(context);
 base.Seed(context);
 }
 public void PerformInitialSetup(AppIdentityDbContext context) {
 // initial configuration will go here
 }
 }
}

ساخت کلاس User Manager
یکی از مهمترین کلاسهای Identity کلاس User Manager (مدیر کاربر) می‌باشد که نمونه‌هایی از کلاس User را مدیریت می‌کند. کلاسی را که تعریف می‌کنیم، باید از کلاس <UserManager<T ارث بری کند که T همان کلاس User می‌باشد. کلاس UserManager خاص EF نمی‌باشد و ویژگی‌های عمومی بیشتری برای ساخت و انجام عملیات بر روی داده‌های کاربر را فراهم می‌نماید.

کلاس UserManager حاوی متدهای بالا است. اگر دقت کنید، می‌بینید که تمامی متدهای بالا به کلمه‌ی Async ختم می‌شوند. زیرا Asp.Net Identity تقریبا کل ویژگیهای برنامه نویسی Async را پیاده سازی کرده است و این بدین معنی است که عملیات میتوانند به صورت غیر همزمان اجرا شده و دیگر فعالیت‌ها را بلوکه نکنند.

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Users.Models;

namespace Users.Infrastructure 
{
 public class AppUserManager : UserManager<AppUser> 
{

 public AppUserManager(IUserStore<AppUser> store)
 : base(store) {
 }
 public static AppUserManager Create( IdentityFactoryOptions<AppUserManager> options, IOwinContext context) 
{
 AppIdentityDbContext db = context.Get<AppIdentityDbContext>();
 AppUserManager manager = new AppUserManager(new UserStore<AppUser>(db));
 return manager;
 }
 }
}

زمانی که Identity نیاز به وهله‌ای از کلاس AppUserManager داشته باشد، متد استاتیک Create را صدا خواهد زد. این عمل زمانی اتفاق می‌افتد که عملیاتی بر روی داده‌های کاربر انجام گیرد. برای ساخت وهله‌ای از کلاس AppUserManager نیاز به کلاس <UserStor<AppUser دارد. کلاس <UserStore<T یک پیاده سازی از رابط <IUserStore<T  توسط  EF میباشد که وظیفه‌ی آن فراهم کننده‌ی پیاده سازی Storage-Specific متدهای تعریف شده در کلاس User Manager (که در اینجا AppUserManager ) می‌باشد. برای ساخت <UserStore<T نیاز به وهله‌ای از کلاس AppIdentityDbContext می‌باشد که از طریق Owin به صورت زیر قابل دریافت است:

AppIdentityDbContext db = context.Get<AppIdentityDbContext>();

یک پیاده سازی از رابط IOwinContext، به عنوان پارامتر به متد Create پاس داده می‌شود. در این پیاده سازی، یک تابع جنریک به نام Get تعریف شده که اقدام به برگشت وهله ای از اشیای ثبت شده‌ی در کلاس شروع Owin می‌نماید.


ساخت کلاس شروع Owin

اگر خاطرتان باشد یک کلید در قسمت AppSettings فایل Web.config به صورت زیر تعریف کردیم:

<add key="owin:AppStartup" value="Users.IdentityConfig" />

قسمت Value کلید بالا از دو قسمت تشکیل شده است: Users بیانگر فضای نام برنامه‌ی شماست و IdentityConfig بیانگر کلاس شروع می‌باشد. (البته شما می‌توانید هر نام دلخواهی را برای کلاس شروع بگذارید. فقط دقت داشته باشید که نام کلاس شروع و مقدار، با کلیدی که تعریف می‌کنید یکی باشد)

Owin مستقل از ASP.NET اعلام شده است و قراردادهای خاص خودش را دارد. یکی از این قراردادها تعریف یک کلاس و وهله سازی آن به منظور بارگذاری و پیکربندی میان افزار و انجام دیگر کارهای پیکربندی که نیاز است، می‌باشد. به طور پیش فرض این کلاس Start نام دارد و در پوشه‌ی App_Start تعریف می‌شود. این کلاس حاوی یک متد به نام Configuration می‌باشد که بوسیله زیرساخت Owin فراخوانی می‌شود و یک پیاده سازی از رابط Owin.IAppBuilder به عنوان آرگومان به آن پاس داده می‌شود که کار پشتیبانی از Setup میان افزار مورد نیاز برنامه را برعهده دارد.

using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Owin;
using Users.Infrastructure;
namespace Users 
{
 public class IdentityConfig 
{
 public void Configuration(IAppBuilder app) 
{
 app.CreatePerOwinContext<AppIdentityDbContext>(AppIdentityDbContext.Create);
 app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
 app.UseCookieAuthentication(new CookieAuthenticationOptions {
 AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
 LoginPath = new PathString("/Account/Login"),
 });
 }
 }
}

رابط IAppBuilder بوسیله تعدادی متد الحاقی که در کلاسهایی که در فضای نام Owin تعریف شده‌اند، تکمیل شده است. متد CreatePerOwinContext کار ساخت وهله‌ای از کلاس AppUserManager و کلاس AppIdentityDbContext را برای هر درخواست بر عهده دارد. این مورد تضمین می‌کند که هر درخواست، به یک داده‌ی تمیز از Asp.Net Identity دسترسی خواهد داشت و نگران اعمال همزمانی و یا کش ضعیف داده نخواهد بود. متد UseCookieAuthentication به Asp.Net Identity می‌گوید که چگونه از کوکی‌ها برای تصدیق هویت کاربران استفاده کند که Optionهای آن از طریق کلاس CookieAuthenticationOptions مشخص می‌شود. مهمترین قسمت در اینجا پروپرتی LoginPath می‌باشد و مشخص می‌کند که کلاینت‌های تصدیق هویت نشده، هنگام دسترسی به یک منبع محافظت شده، به کدام URL هدایت شوند که توسط یک رشته به متد PathString پاس داده می‌شود.

خوب دوستان برپایی سیستم Identity به پایان رسید. انشالله در قسمت بعدی به چگونگی استفاده‌ی از این سیستم خواهیم پرداخت.

مطالب
پیاده سازی عملیات CRUD با استفاده از پروتکل OData
OData  یکی از بهترین روش‌های پیاده سازی RESTful Apis میباشد. Open Data Protocol یا به اصطلاح OData یک data access protocol برای وب میباشد که اجازه‌ی تغییر دادن و نوشتن کوئری درون CRUD مربوطه را میدهد (create - read - update - delete). Asp.Net WebApi از ورژن 3 و 4 این پروتکل بطور کامل پشتیبانی می‌نماید.
در این آموزش ما از WebApi 2.2 , OData V4, Ef 6 استفاده کرده‌ایم.
با استفاده از ویژوال استودیو یک پروژه‌ی Asp.Net را از نوع Empty به نام ProductService میسازیم.

هم چنین در قسمت Add folders and core references تیک گزینه‌ی Web Api را نیز فعال مینماییم.


حال احتیاج به نصب پکیج OData با استفاده از nuget package manager داریم. کافیست دستور زیر را در package manager console وارد نماییم.

Install-Package Microsoft.AspNet.Odata

این دستور آخرین ورژن Odata package را از nuget دانلود مینماید.

بعد از نصب شدن OData نیاز به اضافه کردن یک Model داریم. کلاسی را به نام Product در پوشه‌ی Models میسازیم.

کلاس Product.cs حاوی فیلد‌های زیر است.

namespace ProductService.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
        public string Category { get; set; }
    }
}

پراپرتی Id، کلید این entity است و کلاینت میتواند کوئری را بر روی entity، به وسیله‌ی key بزند. برای مثال برای گرفتن Product با Id برابر 2، باید این url را ارسال نمود "(2)Products/"

پرواضح است که Id در Database به عنوان Primary key در نظر گرفته شده است.

حال احتیاج به نصب Entity Framework داریم که با ارسال دستور زیر از طریق nuget نصب خواهد شد

Install-Package EntityFramework

بعد از نصب کردن ef نیاز به اضافه کردن connection string در web config داریم.

<connectionStrings>
    <add name="ProductsContext" connectionString="Data Source=.; 
        Initial Catalog=ProductsContext; Integrated Security=True;MultipleActiveResultSets=True;"
      providerName="System.Data.SqlClient" />
  </connectionStrings>

الان میتوانیم کلاس ProductsContext را درون پوشه‌ی Models ایجاد نماییم. محتویات آن را به صورت زیر وارد مینماییم

using System.Data.Entity;
namespace ProductService.Models
{
    public class ProductsContext : DbContext
    {
        public ProductsContext() 
                : base("name=ProductsContext")
        {
        }
        public DbSet<Product> Products { get; set; }
    }
}

درون Constructor کلاس ProductsContext، داریم name=ProductsContext که باید برابر name درون connection string باشد.

حال نیاز به کانفیگ OData داریم. درون پوشه‌ی App_Start و کلاس WebApiConfig.cs محتویات زیر را جایگزین متد register نمایید:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        builder.EntitySet<Product>("Products");
        config.MapODataServiceRoute(
            routeName: "ODataRoute",
            routePrefix: null,
            model: builder.GetEdmModel());
    }
}

این کد دو فرآیند زیر را انجام میدهد

1) ساخت Entity Data Model (EDM)

2) اضافه کردن route

EDM یک مدل انتزاعی از data است. EDM برای تولید سند metadata استفاده میشود. کلاس ODataModelBuilder برای ساخت EDM با استفاده از default naming convention میباشد که باعث کاهش کد‌ها میشود. ضمنا کلاس MapODataServiceRoute برای ساخت OData v4 route میباشد. همانگونه که اطلاع دارید، تعریف route برای مدیریت کردن WebApi و چگونگی مسیریابی درخواست‌های http میباشد.

اگر application شما احتیاج به چند OData endpoint داشته باشد، میتوانید برای هر کدام route‌های جدا و همچنین نام یکتایی را برای routeName و routePrefix آن در نظر بگیرید.


اضافه کردن OData Controller

یک Controller، کلاسی برای مدیریت کردن درخواست‌های http میباشد. شما باید Controllerهای مجزایی را برای هر entity set در OData service خود بسازید. در این مقاله Controller مربوط به موجودیت Product را میسازیم.

در Solution Explorer با کلیک راست بر روی پوشه‌ی Controller، کلاسی به نام ProducsController را میسازیم. دقت کنید نام آن حتما باید به Controller ختم شود.

در OData V3 میتوانیم Controller را با استفاده از Scaffolding بسازیم؛ ولی در V4 این ویژگی وجود ندارد!

محتویات زیر را در این کنترلر اضافه مینماییم:

using ProductService.Models;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.OData;
namespace ProductService.Controllers
{
    public class ProductsController : ODataController
    {
        ProductsContext db = new ProductsContext();
        private bool ProductExists(int key)
        {
            return db.Products.Any(p => p.Id == key);
        } 
        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

این مرحله‌ی ابتدایی از پیاده سازی کنترلر میباشد و در قسمت بعد به پیاده سازی CRUD مربوط به آن میپردازیم.


Querying The Entity Set

این 2 متد را به کنترلر خود اضافه مینماییم

[EnableQuery]
public IQueryable<Product> Get()
{
    return db.Products;
}
[EnableQuery]
public SingleResult<Product> Get([FromODataUri] int key)
{
    IQueryable<Product> result = db.Products.Where(p => p.Id == key);
    return SingleResult.Create(result);
}

ویژگی EnableQuery به معنای امکان Query زدن از سمت کلاینت به آن میباشد. FromODataUri نیز برای امکان پاس دادن پارامتر از طریق Uri است.

متد Get بدون پارامتر، قادر به برگرداندن تمامی Product‌ها میباشد و متد Get با پارامتر، قادر به برگرداندن آن Product خاص با استفاده از unique Id است.

در صورت داشتن EnableQuery با استفاده از Query Option هایی مثل filter$ و sort$ و غیره از سمت کلاینت قادر به تغییر دادن کوئری‌های خود هستیم.


Adding and Entity to Entity Set

برای اجازه دادن به کلاینت، جهت اضافه کردن یک Product به دیتابیس، متد Post زیر را اضافه مینماییم

public async Task<IHttpActionResult> Post(Product product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    db.Products.Add(product);
    await db.SaveChangesAsync();
    return Created(product);
}


Updation an Entity

OData از دو روش متفاوت برای Update کردن یک موجودیت استفاده مینماید.

1) Patch : امکان partial update برای موجودیت مربوطه را فراهم میسازد.

2) Put : موجودیت جدید را به صورت کامل جایگزین مینماید.

مشکل روش Put این است که کلاینت مجبور به ارسال تمامی فیلد‌های مربوطه میباشد. حتی آن هایی که اساسا تغییری نکرده‌اند. بنابراین روش Patch ترجیح داده میشود.

در هر صورت ما به پیاده سازی هر دو روش می‌پردازیم:

public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> product)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    var entity = await db.Products.FindAsync(key);
    if (entity == null)
    {
        return NotFound();
    }
    product.Patch(entity);
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(entity);
}
public async Task<IHttpActionResult> Put([FromODataUri] int key, Product update)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    if (key != update.Id)
    {
        return BadRequest();
    }
    db.Entry(update).State = EntityState.Modified;
    try
    {
        await db.SaveChangesAsync();
    }
    catch (DbUpdateConcurrencyException)
    {
        if (!ProductExists(key))
        {
            return NotFound();
        }
        else
        {
            throw;
        }
    }
    return Updated(update);
}

در قسمت Patch کنترلر از <Delta<T استفاده میکند که typeی است برای track کردن تغییرات در مدل مربوطه.


Deleting an Entity

برای حذف هر موجودیت نیز کافیست متد زیر را به کنترلر خود اضافه نمایید:

public async Task<IHttpActionResult> Delete([FromODataUri] int key)
{
    var product = await db.Products.FindAsync(key);
    if (product == null)
    {
        return NotFound();
    }
    db.Products.Remove(product);
    await db.SaveChangesAsync();
    return StatusCode(HttpStatusCode.NoContent);
}

من چند رکورد تستی را به صورت زیر وارد کرده‌ام:

حال پروژه‌ی خود را run نموده و آدرس زیر را وارد نمایید:

http://localhost:YourPort/Products

پاسخ، مجموعه‌ای از entity‌های زیر خواهد بود:

{
  "@odata.context":"http://localhost:4516/$metadata#Products","value":[
    {
      "Id":1,"Name":"Ali","Price":2.00,"Category":"aaa"
    },{
      "Id":2,"Name":"Reza","Price":1.00,"Category":"bbb"
    },{
      "Id":3,"Name":"Ahmad","Price":0.00,"Category":"ccc"
    }
  ]
}

شما میتوانید از هر کدام از فیلتر‌های زیر برای کوئری زدن از کلاینت به سمت سرور استفاده نمایید. بطور مثال هر کدام از اینها پاسخ متفاوت و مربوط به خود را برگشت میدهد:

/Products(2)

Productی با آی دی 2 را بر میگرداند.

/Products?$filter=Id gt 1

محصولی را با آی دی بزرگتر از 1، بر میگرداند.

Products?$select=Name

روی محصولات select زده و فقط فیلد Name آن‌ها را بر میگرداند.

Products?$select=Name,Price

آرایه‌ای از objectهایی با پراپرتی Name و Price را بر میگرداند.

/Products?$top=3

فقط 3 رکورد اول را بر میگرداند.


همانطور که ملاحظه میفرمایید، استفاده از OData باعث کمتر شدن کد‌های سمت سرور و همچنین امکان کوئری زدن از سمت کلاینت به سمت سرور را مهیا می‌کند.

بعد از خواندن این مقاله ممکن است به این مساله فکر کنید که این کار باعث کاهش امنیت میشود. باید عرض کنم که امکانات زیادی برای محدود کردن کوئری‌ها، فراهم شده است و هیچ نگرانی از این بابت وجود ندارد. بطور مثال میتوانید تعیین کنید که از entity مربوطه فقط حداکثر 3 پراپرتی قابلیت کوئری زدن را دارند؛ یا اینکه حداکثر در هر کوئری، 10 رکورد قابلیت پاسخ دادن خواهد داشت.

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

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

مطالب
مشکل ی و ک فارسی و عربی در یک دیتابیس اس کیوال سرور

دیروز به من اطلاع دادند که در یکی از برنامه‌ها دو تا گروه "تاسیسات مکانیکی" پیدا شده!!
تاسیسات مکانیکی
تاسیسات مکانیکی

استاندارد این شرکت، استفاده از kbdfa.dll مخصوص و نسبتا قدیمی است. بنابراین استاندارد مورد استفاده همان ی و ک عربی است. (کاری ندارم خوب است یا بد، یا باید اینطور باشد یا نه، بحث این است که فعلا اینطور است و قرار نیست چیزی عوض بشود!)
در مثال فوق، ی و ک عبارت دوم فارسی است. یعنی نصب kbdfa.dll روی ویندوز تازه نصب شده، فراموش شده بوده.

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

public string SafeFarsiStr(string input)
{
return input.Replace("ی", "ی").Replace("ک", "ک");
}

ب) خوب، الان که این یکسان سازی صورت نگرفته چه باید کرد؟
اسکریپتی را تهیه کرده‌ام (مخصوص SQL Server 2005 به بعد) به صورت زیر که این تبدیل را برای شما انجام می‌دهد.
به صورت خودکار تمامی فیلدهای متنی کلیه جداول دیتابیس جاری شما را یافته و ی و ک آن‌ها را یکسان می‌کند. البته همانطور که عرض شد، مطابق استاندارد این شرکت و استفاده از فایل kbdfa.dll قدیمی مورد استفاده، تمام ی و ک های فارسی به عربی تبدیل می‌شوند.

--اسکریپتی برای یک دست سازی ی و ک در تمامی رکوردهای تمامی جداول دیتابیس جاری
-- اسکریپت زیر ی و ک فارسی را به عربی تبدیل می‌کند
-- در صورت نیاز به حالت عکس ، جای مقادیر عددی یونیکد را تعویض نمائید

USE TestDb;

DECLARE @Table NVARCHAR(MAX),
@Col NVARCHAR(MAX)

DECLARE Table_Cursor CURSOR
FOR
--پیدا کردن تمام فیلدهای متنی تمام جداول دیتابیس جاری
SELECT a.name, --table
b.name --col
FROM sysobjects a,
syscolumns b
WHERE a.id = b.id
AND a.xtype = 'u' --User table
AND (
b.xtype = 99 --ntext
OR b.xtype = 35 -- text
OR b.xtype = 231 --nvarchar
OR b.xtype = 167 --varchar
OR b.xtype = 175 --char
OR b.xtype = 239 --nchar
)

OPEN Table_Cursor FETCH NEXT FROM Table_Cursor INTO @Table,@Col
WHILE (@@FETCH_STATUS = 0)
BEGIN
EXEC (
'update [' + @Table + '] set [' + @Col +
']= REPLACE(REPLACE(CAST([' + @Col +
'] as nvarchar(max)) , NCHAR(1740), NCHAR(1610)),NCHAR(1705),NCHAR(1603)) '
)

FETCH NEXT FROM Table_Cursor INTO @Table,@Col
END CLOSE Table_Cursor DEALLOCATE Table_Cursor

توضیحات و نکاتی در مورد اسکریپت فوق:
الف) برای آشنایی با انواع XType Datatype مورد استفاده در کوئری فوق به این آدرس مراجعه نمائید.
ب) همانطور که در مطالب قبلی این وبلاگ نیز ذکر شد، امکان استفاده از تابع replace بر روی فیلدهای text و ntext وجود ندارد. هیچ اشکالی ندارد! تمام آن‌ها به nvarchar از نوع max دار cast شده و این مشکل به این صورت حل می‌شود.
ج) اس کیوال سرور اجازه تعریف یک جدول یا فیلد را به صورت متغیر بکار رفته در یک کوئری T-SQL نمی‌دهد. برای حل این موضوع باید عبارت SQL مورد نظر را به صورت یک رشته درآورد و سپس exec کرد.
د) مجبور شدم از معاد‌ل‌های عددی برای دقت بیشتر کار استفاده کنم



(در کل از تابع UNICODE اس کیوال سرور برای بدست آوردن این اعداد می‌توان استفاده کرد)

تذکر: این اسکریپت بر روی یک دیتابیس کاری تست شده و نتیجه رضایت بخش بوده؛ اما اگر شما روزی خواستید از آن استفاده کنید، حتما full backup را قبل از اجرای آن فراموش نکنید و پس از اجرا، تابع SafeFarsiStr فوق را برای ادامه کار حتما لحاظ نمائید یا از یک kbdfa.dll هماهنگ استفاده کنید.


مطالب
LINQ to Sharepoint Class
شیرپوینت قابلیت استفاده از دستورات LINQ را برای دسترسی به لیست‌های خود می‌دهد . این قابلیت جایگزین خوبی برای استفاده سنتی از CAML queries می باشد. (Collaborative  Application Markup Language (CAML برای تعریف کوئری‌ها درون لیست داده‌ها استفاده می‌شود و بر مبنای XML می‌باشد بیشتر 
برای این منظور می‌توان از دستور زیر استفاده کرد تا Sharepoint Foundation برای شما کلاسی بسازد تا بتوانید به تمام اعضای لیست داده‌های Sharepoint دسترسی داشته باشید  
لازم است بدانید که برای ساخت این کلاس از LINQ Provider Code Generator خود شیرپوینت به نام SPMETAL.EXE که در پوشه bin در زیر مجموعه 14 قرار دارد استفاده می‌شود . 

spmetal.exe /web:http://DOMAIN/code:c:\EntityModuleSharePoint.cs /user:DOMAIN\USERNAME/password:********** /namespace:EntityModuleSharePoint 


توجه داشته باشید که پسوند فایلی که نوشته اید مشخص می‌کند که دستورات بر مبنای چه زبانی ساخته شوند (قسمت قرمز رنگ بعد از سوییچ code ) مثلا با تغیر پسوند به VB فایل ایجاد شده با دستورات vb قابل استفاده است  

برای مشاهده یک مثال از این مورد ، به اینجا مراجعه نمایید 
مطالب
بررسی کارآیی کوئری‌ها در SQL Server - قسمت ششم - بررسی عملگرهای دسترسی به داده‌ها در یک Query Plan
پس از آشنایی مقدماتی با نحوه‌ی خواندن یک Query Plan، اکنون نوبت به بررسی عملگرهایی است که در آن مشاهده می‌شوند و همچنین تغییرات در کوئری‌ها چگونه بر روی آن‌ها تاثیر گذاشته و آن‌ها را تغییر می‌دهند و این تغییرات چه تاثیری را بر روی کارآیی خواهند داشت.


عملگرهای Scans و Seeks

در حالت کلی می‌توان دو نوع جدول بدون و با ایندکس را درنظر گرفت. در حالت جداول بدون ایندکس، برای جستجوی اطلاعات نیاز به Table Scan وجود دارد و برعکس آن شامل یک Clustered index scan خواهد بود. گاهی از اوقات Clustered index scanها بهترین روش دریافت اطلاعات هستند و گاهی از اوقات خیر و نیاز به بررسی بیشتری دارند. بنابراین قانون کلی، حذف آن‌ها به محض مشاهده، نیست.
نوع دیگر عملگرهای دسترسی به داده‌ها، Seeks هستند که شامل Clustered index seeks و Non-clustered index seeks می‌شوند. در بسیاری از موارد عنوان می‌شود که Seeks کارآیی بهتری را به همراه دارند. هرچند این مورد نیاز به بررسی بیشتری دارد که در ادامه با مثال‌هایی آن‌ها را مرور خواهیم کرد.


بررسی عملگر Table scan در یک Query Plan

در ادامه تعدادی از عملگرهای مرتبط با data access را از لحاظ نحوه‌ی انتخاب و تغییر آن‌ها توسط بهینه ساز کوئری‌های SQL Server بررسی می‌کنیم. برای این منظور ابتدا در management studio از منوی Query، گزینه‌ی Include actual execution plan را انتخاب می‌کنیم. سپس کوئری‌های زیر را اجرا می‌کنیم:
SET STATISTICS IO ON;
GO
SET STATISTICS TIME ON;
GO

SELECT *
INTO [Sales].[Copy_Orders]
FROM [Sales].[Orders];
GO

SELECT
    [CustomerID],
    [OrderID],
    [OrderDate]
FROM [Sales].[Copy_Orders]
WHERE [CustomerID] > 550;
GO
در اینجا در ابتدا، تمام رکوردهای جدول [Sales].[Orders]، به جدول [Sales].[Copy_Orders] کپی می‌شوند. سپس یک کوئری را بر روی این جدول کپی، اجرا کرده‌ایم.


همانطور که مشاهده می‌کنید، برای برآورده کردن قسمت where این کوئری، یک Table Scan صورت گرفته‌است؛ چون این جدول کپی، به همراه هیچ ایندکسی نیست. به همین جهت برای یافتن رکوردهای مدنظر، راه دیگری بجز اسکن کل جدول بانک اطلاعاتی وجود ندارد که بسیار ناکارآمد است.
همچنین اگر به برگه‌ی messages دقت کنیم، با توجه به روشن بودن STATISTICS IO، میزان logical reads نیز قابل مشاهده‌است:
(33035 rows affected)
Table 'Copy_Orders'. Scan count 1, logical reads 689, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
به علاوه اجرای آن نیز کمی بیشتر از نیم ثانیه، طول کشیده‌است:
SQL Server Execution Times:
CPU time = 79 ms,  elapsed time = 762 ms.


بررسی عملگر Index Seek در یک Query Plan

اکنون سؤال اینجا است که آیا می‌توان این وضعیت را بهبود بخشید؟
بله. برای این منظور یک NONCLUSTERED INDEX را بر روی جدول کپی، ایجاد می‌کنیم؛ به نحوی که CustomerID لحاظ شده‌ی در قسمت where کوئری را پوشش دهد:
CREATE NONCLUSTERED INDEX [IX_Copy_Orders_CustomerID]
ON [Sales].[Copy_Orders] (
[CustomerID]
)
INCLUDE (
[OrderID], [OrderDate]
);
GO
چون مطابق کوئری، [OrderID] و [OrderDate] در قسمت where ذکر نشده‌اند، در اینجا INCLUDE شده‌اند.

در ادامه مجددا همان کوئری را اجرا می‌کنیم:
SELECT
    [CustomerID],
    [OrderID],
    [OrderDate]
FROM [Sales].[Copy_Orders]
WHERE [CustomerID] > 550;
GO
که سبب تولید کوئری پلن زیر می‌شود:


اینبار عملگر Table Scan قبلی به یک عملگر Index Seek بر روی NONCLUSTERED INDEX تعریف شده، تغییر کرده‌است و اگر به آمار I/O آن دقت کنیم، logical reads 106 قابل مشاهده‌است که بهبود قابل ملاحظه‌ای است نسبت به عدد 689 قبلی.


بررسی عملگر Clustered index scan در یک Query Plan

در ادامه همین کوئری را بر روی جدول [Sales].[Orders] اصلی اجرا می‌کنیم:
SELECT
    [CustomerID],
    [OrderID],
    [OrderDate]
FROM [Sales].[Orders]
WHERE [CustomerID] > 550;
GO
که به صورت پیش‌فرض شامل این ایندکس‌ها است:


اجرای کوئری فوق، چنین کوئری پلنی را تولید می‌کند:


جدول [Sales].[Orders]، یک CLUSTERED INDEX را بر روی [OrderID] دارد و یک NONCLUSTERED INDEX را بر روی [CustomerID].
در کوئری پلن تولید شده، یک Clustered index scan مشاهده می‌شود. علت اینجا است که هرچند در جدول [Sales].[Orders] یک NONCLUSTERED INDEX بر روی  [CustomerID] تعریف شده‌است:
CREATE NONCLUSTERED INDEX [FK_Sales_Orders_CustomerID] ON [Sales].[Orders]
(
[CustomerID] ASC
)
اما قسمت INCLUDE ایندکس قبلی را که تعریف کردیم، ندارد و به همراه [CustomerID] و [OrderDate] نیست. به همین جهت اینبار logical reads 692 است.

بنابراین وجود عملگر Clustered index scan در یک کوئری پلن، یعنی نیاز به خواندن و اسکن کل جدول وجود دارد. برای اثبات آن، همین کوئری قبلی را که بر روی [Sales].[Orders] انجام دادیم، اینبار بدون قسمت where آن اجرا کنید. یعنی کوئری بر روی کل جدول انجام شود:
SELECT
    [CustomerID],
    [OrderID],
    [OrderDate]
FROM [Sales].[Orders]
سپس به برگه‌ی messages مراجعه کرده و عدد logical reads آن‌را مشاهده کنید. این عدد دقیقا با عدد logical reads کوئری where دار، یکی است؛ که بیانگر اسکن کامل جدول در حالت Clustered index scan است.

سؤال: آیا Clustered index scan همواره کل یک جدول را اسکن می‌کند؟
پاسخ: خیر. اگر یک کوئری برای مثال دارای top/min/max باشد، کل جدول اسکن نخواهد شد:
SELECT TOP 10
    [CustomerID],
    [OrderID],
    [OrderDate]
FROM [Sales].[Orders]
WHERE [CustomerID] > 550;
تفاوت این کوئری با کوئری‌های قبلی، در داشتن یک top 10 است. اگر آن‌را اجرا کنیم، به کوئری پلن زیر خواهیم رسید:


هرچند در اینجا هم یک Clustered index scan صورت گرفته، اما اگر به برگه‌ی messages آن مراجعه کنیم، آمار I/O آن بیانگر تنها logical reads 5 است که معادل اسکن کل جدول نیست:
(10 rows affected)
Table 'Orders'. Scan count 1, logical reads 5, physical reads 0, read-ahead reads 510, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.


مقایسه‌ی عملگرهای Index Scan و Index Seek

ابتدا کوئری زیر را اجرا می‌کنیم:
SELECT
    [CustomerID],
    [OrderID]
FROM [Sales].[Orders]
WHERE [OrderID] > 30000;
این کوئری با کوئری قبلی از لحاظ قسمت select اندکی متفاوت بوده و در آن OrderDate حذف شده‌است. در قسمت where نیز کوئری بر روی OrderID صورت گرفته‌است.
در این جدول ایندکسی بر روی CustomerID وجود دارد و همچنین کلید اصلی جدول، OrderID است.

پس از اجرای این کوئری، به کوئری پلن زیر خواهیم رسید:


که بیانگر یک Index Scan است و نکته‌ی جالب آن، استفاده‌ی از ایندکس FK_Sales_Orders_CustomerID می‌باشد (نام این شیء، ذیل آیکن عملگر، مشخص است). یعنی SQL Server در اینجا از یک non-clustered index تعریف شده‌ی بر روی CustomerID استفاده کرده‌است.
اکنون اگر OrderID را تغییر دهیم چه اتفاقی رخ می‌دهد؟
SELECT
    [CustomerID],
    [OrderID]
FROM [Sales].[Orders]
WHERE [OrderID] > 60000;
اینبار به یک clustered index seek رسیدیم که بر روی کلید اصلی جدول یا همان PK_Sales_Orders که ذیل عملگر مشخص شده، رخ داده‌است:


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

اگر این دو کوئری را با هم اجرا کنیم (در طی یک batch)، به پلن مقایسه‌ای زیر خواهیم رسید که در آن هزینه‌ی Index Scan بیشتر است از clustered index seek:


به همراه آمار CPU و I/O ای به صورت زیر که اولی مرتبط است با index scan و دومی با clustered index seek:
(43595 rows affected)
Table 'Orders'. Scan count 1, logical reads 191, physical reads 1, read-ahead reads 182, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 SQL Server Execution Times:
CPU time = 31 ms,  elapsed time = 754 ms.


(13595 rows affected)
Table 'Orders'. Scan count 1, logical reads 131, physical reads 0, read-ahead reads 127, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
 SQL Server Execution Times:
CPU time = 16 ms,  elapsed time = 276 ms.
به همین جهت است که عنوان می‌شود، scanها خوب نیستند و seekها بهترند.
مطالب
اندازه گیری کارآیی پرس و جوها با استفاده از SET STATISTICS TIME
یکی از وظایف اصلی مدیر و یا توسعه دهنده یک بانک اطلاعاتی، نوشتن کدهای T-SQL و اندازه‌گیری عملکرد آنها می‌باشد. ابزارهای مختلفی برای انجام این کار وجود دارد، چه آنهایی که در خود SQL Server بصورت محلی وجود دارند و چه آنهایی که توسط شرکت‌های ثالث ارائه می‌شوند. اما مسئله مهمی که باید در نظر بگیرید چگونگی نوشتن یک پرس و جو (Query) و اندازه گیری کارآیی آن می‌باشد و اینکه باید روی چه مواردی متمرکز شد. در اکثر مواقع گرفتن زمان اجرای یک پرس و جو تا اندازه‌ای خوب می‌باشد. یکی از مواردی که باید روی آن متمرکز شد منابع استفاده شده توسط سرور می‌باشد، درحالیکه زمان اجرای پرس وجو به پارامترهای دیگری همچون بار سرور نیز بستگی دارد. علاوه بر استفاده از پروفایلر و نقشه اجرای کوئری (Execution Plan) ، می‌توانید از SET STATISTICS TIME نیز استفاده نمایید.
 SET STATISTICS TIME تنظیمی است که برای اندازه گیری منابع مورد نیاز اجرای یک پرس و جو به شما کمک می‌کند. SET STATISTICS TIME آمار مربوط به زمان تجزیه و تحلیل (Parse)، کامپایل و اجرای هر دستور در یک پرس و جو را نمایش می‌دهد. راه‌های مختلف اندکی برای مقایسه آماری دو پرس و جو و انتخاب بهترین آنها برای استفاده وجود دارند.  
برای روشن کردن این تنظیم دو راه وجود دارد. ابتدا اینکه از دستور Set برای روشن و خاموش کردن استفاده نمایید و یا اینکه از طریق Query Analyzer اقدام به انجام این کار نمایید. 
SET STATISTICS TIME ON
برای اینکه بتوانید آمارهای این تنظیمات را مشاهده کنید می‌بایست قبل از اجرای پرس و جو تنظیم مورد نظر را روشن نمایید. در نظر داشته باشید که با روشن کردن این تنظیم، برای تمامی پرس و جوهای مربوط به آن جلسه (Session) روشن خواهد ماند تا زمانیکه آنرا خاموش نمایید. 
SELECT ProductID, StartDate, EndDate, StandardCost 
FROM Production.ProductCostHistory
WHERE StandardCost < 500.00; 
با اجرای دستورات بالا خروجی آن بصورت زیر می‌باشد:
SQL Server parse and compile time: 
   CPU time = 0 ms, elapsed time = 1 ms.

(269 row(s) affected)

SQL Server Execution Times:
   CPU time = 1 ms,  elapsed time = 2 ms. 
زمان صرف شده برای اجرای یک کوئری به دو بخش تقسیم می‌شود:
  • زمان کامپایل و تجزیه و تحلیل ( parse and compile time)  زمانیکه یک کوئری را برای اجرا به SQL Server  ارائه می‌دهید، SQL Server آنرا از نظر خطای نحوی بررسی می‌نماید و بهینه ساز یک نقشه بهینه را برای اجرا تولید می‌نماید. اگر به خروجی نگاه کنید، زمان پردازش ( CPU time) و زمان سپری شده ( elapsed time)  را نشان می دهد. منظور از زمان پردازش زمان واقعی صرف شده روی پردازنده می‌باشد و زمان سپری شده اشاره به زمان تکمیل شدن عملیات کامپایل و تجزیه و تحلیل می‌باشد. تفاوت بین زمان پردازش و زمان سپری شده ممکن است زمان انتظار در صف برای گرفتن پردازنده و یا انتظار برای تکمیل عملیات IO باشد. 

  • زمان اجرا  ( Execution Times) : این زمان اشاره به زمان سپری شده برای تکمیل اجرای نقشه کامپایل شده دارد. زمان پردازش اشاره به زمان واقعی صرف شده روی پردازنده دارد و زمان سپری شده نیز مجموع زمان صرف شده تا تکمیل اجرای دستور که شامل زمان انتظار برای تکمیل عملیات IO و زمان صرف شده برای انتقال خروجی به کلاینت می‌باشد، دارد. زمان پردازش می‌تواند به عنوان مبنایی برای اندازه‌گیری کارآیی مورد استفاده قرار بگیرد. این مقدار در اجراهای مختلف تفاوت چندانی با هم ندارند جز اینکه کوئری و یا داده‌ها تغییر نمایند. توجه نمایید که زمان براساس میلی ثانیه می‌باشد. زمان سپری شده نیز به فاکتورهایی مانند بارگذاری روی سرور، بارگذاری IO، و پهنای باند بین سرور و کلاینت وابسته می‌باشد. بنابراین همیشه زمان پردازش به عنوان مبنایی برای اندازه‌گیری کارایی استفاده می‌شود .

در این بخش به بررسی SET STATISTICS TIME  در SQL Server پرداختیم. در بخش بعدی به بررسی   SET STATISTICS IO  برای اندازه گیری کارایی پرس و جوها از نظر میزان استفاده IO خواهیم پرداخت.
نظرات مطالب
آشنایی با FileTable در SQL Server 2012 بخش 2
فکر می‌کنم برای افزودن این قابلیت به دیتابیس دسترسی به دیتابیس master نیاز باشه که هاست‌ها این امکان رو نمیدن.
مطالب
گروه بندی اطلاعات و گزارشات Master-Details در PdfReport
اگر به بانک اطلاعاتی مثال‌های همراه سورس‌های PdfReport در مسیر Bin\Data\blogs.sqlite مراجعه کنید، دو جدول والدین و فرزندان هم در آن وجود دارند:



بر این اساس قصد داریم رابطه یک به چند فوق را گروه بندی شده نمایش دهیم:


(البته این اعداد و اطلاعات، به صورت اتفاقی تولید شده‌اند و الزامی ندارد که والد متولد 2002 هنوز والد شده باشد؛ یا اینکه فرزندی متولد 2003 داشته باشد!)

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

سورس کامل این مثال را در ادامه مشاهده می‌کنید:
using System;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.MasterDetails
{
    public class MasterDetailsPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.LeftToRight);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\arial.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesHeader(header =>
            {
                header.CustomHeader(new MasterDetailsHeaders { PdfRptFont = header.PdfFont });
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .MainTableTemplate(t => t.BasicTemplate(BasicTemplate.SilverTemplate))
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.GroupsPreferences(new GroupsPreferences
                {
                    GroupType = GroupType.HideGroupingColumns,
                    RepeatHeaderRowPerGroup = true,
                    ShowOneGroupPerPage = false,
                    SpacingBeforeAllGroupsSummary = 5f,
                    NewGroupAvailableSpacingThreshold = 170
                });
            })
            .MainTableDataSource(dataSource =>
            {
                dataSource.GenericDataReader(
                   providerName: "System.Data.SQLite",
                   connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                   sql: @"select 
                            tblParents.BirthDate as ParentBirthDate,
                            tblParents.Name as ParentName,
                            tblParents.LastName as ParentLastName,
                            tblKids.Name as KidName,
                            tblKids.BirthDate as KidBirthDate
                            from tblParents
                                left outer join tblKids
                                     on tblKids.ParentId = tblParents.Id
                            order by 
                                tblParents.Name,
                                tblParents.LastName,
                                tblKids.Name"
               );
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Left);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("#");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("ParentBirthDate");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("ParentBirthDate");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        var date1 = (DateTime)val1;
                        var date2 = (DateTime)val2;
                        return date1.Year == date2.Year && date1.Month == date2.Month && date1.Day == date2.Day;
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("ParentName");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(2);
                    column.Width(2);
                    column.HeaderCell("ParentName");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("ParentLastName");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("ParentLastName");
                    column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("KidName");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("Child Name");
                    column.IsVisible(true);
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("KidBirthDate");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.Order(5);
                    column.Width(2);
                    column.HeaderCell("BirthDate");
                    column.IsVisible(true);
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(e => e.ToExcel())
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptMasterDetailsSample.pdf"));
        }
    }
}
به همراه سر ستون‌های مجزای هر گروه و صفحه:
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.ColumnsItemsTemplates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;

namespace PdfReportSamples.MasterDetails
{
    public class MasterDetailsHeaders : IPageHeader
    {
        public IPdfFont PdfRptFont { set; get; }

        public PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData)
        {
            var parentName = newGroupInfo.GetSafeStringValueOf("ParentName");
            var parentLastName = newGroupInfo.GetSafeStringValueOf("ParentLastName");
            var parentBirthDate = newGroupInfo.GetSafeStringValueOf("ParentBirthDate");

            var table = new PdfPTable(relativeWidths: new[] { 1f, 5f }) { WidthPercentage = 100 };
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "Name:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = parentName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "Last Name:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = parentLastName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
            table.AddSimpleRow(
               (cellData, cellProperties) =>
               {
                   cellData.Value = "Birth Date:";
                   cellProperties.PdfFont = PdfRptFont;
                   cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
               },
               (cellData, cellProperties) =>
               {
                   cellData.Value = parentBirthDate;
                   cellProperties.PdfFont = PdfRptFont;
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
               });
            return table.AddBorderToTable(borderColor: BaseColor.LIGHT_GRAY, spacingBefore: 5f);
        }

        public PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData)
        {
            var table = new PdfPTable(numColumns: 1) { WidthPercentage = 100 };
            table.AddSimpleRow(
               (cellData, cellProperties) =>
               {
                   cellData.CellTemplate = new ImageFilePathField();
                   cellData.Value = AppPath.ApplicationPath + "\\Images\\01.png";
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Center;
               });
            table.AddSimpleRow(
               (cellData, cellProperties) =>
               {
                   cellData.Value = "Family rpt";
                   cellProperties.PdfFont = PdfRptFont;
                   cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                   cellProperties.HorizontalAlignment = HorizontalAlignment.Center;
               });
            return table.AddBorderToTable();
        }
    }
}
توضیحات:
- منبع داده مورد استفاده در اینجا از نوع GenericDataReader است؛ جهت خواندن رکوردهای بانک اطلاعاتی SQLite ذکر شده در ابتدای بحث. (دو مثال دیگر هم به پوشه مثال‌های سورس‌های PdfReport اضافه شده‌اند به نام‌های Grouping و WrapGroupsInColumns که به همین موضوع گروه بندی می‌پردازند؛ البته با استفاده از StronglyTypedList‌ها. ولی درکل مفاهیم و اصول آن‌ها یکی است.)
select 
          tblParents.BirthDate as ParentBirthDate,
          tblParents.Name as ParentName,
          tblParents.LastName as ParentLastName,
          tblKids.Name as KidName,
          tblKids.BirthDate as KidBirthDate
          from tblParents
                  left outer join tblKids
                        on tblKids.ParentId = tblParents.Id
                            order by 
                                tblParents.Name,
                                tblParents.LastName,
                                tblKids.Name
در کوئری فوق (و کلا گروه بندی اطلاعات) دو نکته حائز اهمیت است:
الف) چون قرار است اطلاعات بر اساس مشخصات والدین و فرزندان آن‌ها گروه بندی شود، نیاز است حتما order by  و مرتب سازی رکوردها قید گردد.
ب) در PdfReport نمی‌توانید در خواص معرفی شده جهت تعریف ستون‌ها، از نام‌های تکراری استفاده کنید. برای رفع این مشکل استفاده از Alias پیشنهاد می‌شود؛ مانند:
tblParents.Name as ParentName,
tblKids.Name as KidName,
- مشخص سازی خاصیت و ستونی که قرار است در گروه بندی شرکت کند بسیار ساده است:
column.Group(true,
                    (val1, val2) =>
                    {
                        return val1.ToString() == val2.ToString();
                    });
در اینجا به کمک متد Group، قابلیت گروه بندی بر روی این ستون فعال شده و سپس باید فرمولی را جهت مشخص سازی حد و مرز گروه مشخص کنیم. برای مثال در اینجا اگر مقادیر ردیف جاری (val2) و ردیف قبلی (val1) یکسان نبودند، یعنی گروه خاتمه یافته و گروه جدیدی شروع می‌شود (به همین جهت عنوان شد که مرتب سازی اطلاعات ضروری است).

- تنظیم دیگری را که در اینجا می‌توان ذکر کرد، مورد ذیل است:
                table.GroupsPreferences(new GroupsPreferences
                {
                    GroupType = GroupType.HideGroupingColumns,
                    RepeatHeaderRowPerGroup = true,
                    ShowOneGroupPerPage = false,
                    SpacingBeforeAllGroupsSummary = 5f,
                    NewGroupAvailableSpacingThreshold = 170
                });
به این ترتیب می‌توان مشخص کرد که آیا باید ستون‌های دخیل در گروه بندی، در گزارش نمایش داده شوند یا خیر (GroupType.HideGroupingColumns)، آیا سر ستون هر جدول، به ازای هر گروه باید تکرار شود؟ (RepeatHeaderRowPerGroup)، آیا در هر صفحه یک گروه نمایش داده شود (ShowOneGroupPerPage) یا اینکه گروه‌ها به صورت متوالی در صفحات درج شوند. توسط SpacingBeforeAllGroupsSummary، فاصله جمع نهایی تمام گروه‌ها از آخرین گروه نمایش داده شده مشخص می‌شود. به کمک NewGroupAvailableSpacingThreshold مشخص می‌کنیم که در چه فاصله‌ای از انتهای صفحه، گروه جدیدی نباید درج شود و این گروه باید به صفحه بعدی منتقل شده و از آنجا شروع شود.

- اگر به تصویر ابتدای مطلب دقت کرده باشید، علاوه بر هدر صفحه، هر گروه نیز یک هدر مجزا دارد. برای طراحی آن باید اینترفیس IPageHeader را پیاده سازی کرد که نمونه‌ای از آن‌را در کلاس MasterDetailsHeaders فوق مشاهده می‌کنید.
        public PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData)
        {
            var parentName = newGroupInfo.GetSafeStringValueOf("ParentName");
            var parentLastName = newGroupInfo.GetSafeStringValueOf("ParentLastName");
            var parentBirthDate = newGroupInfo.GetSafeStringValueOf("ParentBirthDate");

            var table = new PdfPTable(relativeWidths: new[] { 1f, 5f }) { WidthPercentage = 100 };
            table.AddSimpleRow(
                (cellData, cellProperties) =>
                {
                    cellData.Value = "Name:";
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                },
                (cellData, cellProperties) =>
                {
                    cellData.Value = parentName;
                    cellProperties.PdfFont = PdfRptFont;
                    cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                });
ساختار آن هم بسیار ساده است. توسط  newGroupInfo می‌توان به اطلاعات گروه جدید، دسترسی یافت. برای مثال در اینجا اطلاعات والد گروه جدید در حال تهیه، دریافت شده و سپس در ردیف‌های یک جدول دو ستونه درج می‌شود. در ستون اول آن یک برچسب و در ستون دوم، مقدار دریافتی نمایش داده شده است و همینطور الی آخر برای سایر ردیف‌ها.
نظرات مطالب
پشتیبانی توکار از انجام کارهای پس‌زمینه در ASP.NET Core 2x
- یک وظیفه در اینجا همیشه در حال اجرا نیست. فقط زمانیکه به تنظیمات خاصیت Schedule آن برسد، اجرا می‌شود. تمام وظایف پشت صحنه به همین صورت اجرا و مدیریت می‌شوند. یک حلقه مخصوص بررسی رسیدن به زمان‌بندی مدنظر وجود دارد و سپس اجرای آن وظیفه‌ی خاص. نمونه‌ی دیگر آن پروژه‌ی « DNTScheduler.Core » است که معادل NET Core. مطلب «انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler» هست. 
- برای غیرفعال کردن یک Task در مطلب جاری، باید آن‌را از لیست سرویس‌های ثبت شده‌ی سیستم حذف کنید (و یا برای معرفی آن به سیستم باید به سیستم تزریق وابستگی‌ها توسط services.AddHostedService اضافه شود).
public static class ServiceCollectionExtensions
{
    public static IServiceCollection Remove<T>(this IServiceCollection services)
    {
        var serviceDescriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(T));
        if (serviceDescriptor != null) services.Remove(serviceDescriptor);

        return services;
    }
}
و یا با توجه به اینکه این وظایف به صورت یک سرویس ثبت می‌شوند، می‌توانید یک سرویس سفارشی فعال یا غیرفعالسازی را تعریف کنید و آن‌را به سازنده‌ی این وظایف تزریق و استفاده کنید. برای مثال زمانیکه حلقه‌ی انجام وظایف به به تنظیمات خاصیت Schedule رسید، متد ScheduledExecuteInScope را اجرا می‌کند. در این متد فرصت خواهید داشت تا سرویس سفارشی جدید تزریق شده را بررسی کرده و از فعال بودن یا نبودن این وظیفه مطلع شوید (برای مثال این سرویس بر اساس نامی که به آن ارسال می‌کنید، به بانک اطلاعاتی مراجعه کرده و روشن و یا خاموش بودن آن‌را بررسی کند. تنظیم بانک اطلاعاتی آن‌را هم واگذار کنید به قسمت مدیریتی برنامه).
اشتراک‌ها
پلتفرم خودکار یک پلتفرم رایگان و متن باز (Open Source) برای تولید نرم افزار تحت وب با تکنولوژی مایکروسافت
پلتفرم خودکار یه پروژه متن بازه و ایده اصلی اش تولید یک نرم افزار تحت وب از صفر تا صد به صورت آنلاین و بدون نیاز به ابزار هایی مثل ویژوال استدیو Sql Management Studio و TFS  و غیره و تنها با یک مرورگر ساده مثل Chrome .


پلتفرم خودکار  همانند ویژوال استدیو دو حالت Release و دیباگ دارد که این امکان را به برنامه نویسان می‌دهد که کدهای اجرایی در سمت سرور و کلاینت در دو حالت دیباگ و Release کاملا مختلف و مجزا باشند.

یعنی شما برای ساخت یک وب سایت می‌تونید تمام کدهای سمت سرور و کلاینت و دیتابیس رو از طریق یک مرورگر وب بنویسید و اگه نیاز به کامپایل باشه , خود پلتفرم این کار رو انجام میده.
تعدادی از این کدها : C# VB.Net SQL CSS JavaScript SASS LESS Coffee و غیره

اما غیر از اینها پلتفرم خودکار شامل یه سورس کنترل و مدیریت ورژن و Build System و Load Module  اختصاصی هم میشه .یعتی عملا شما نیازی به سورس کنترل‌های آنلاین
مثل GitHub , ... را ندارید و می‌تونید دسترسی ویرایش و یا Build و یا Test و یا اجرا رو در سطح یک خط کد تا یک پروژه به طور کامل به سایر برنامه نویسان بدهید.

برای طراحی و ساخت دیتابیس تون و مدیریت Migration‌‌ها هم یک ابزار انلاین داره که پشت صحنه از Entity FrameWork استفاده می‌کنه و تمامی امکانات Entity FrameWork   برای ساخت و پشتیبانی دیتابیس رو به صورت آنلاین در اختیار شما می‌زاره.

وابسته به فریم ورک خاصی در سمت کلاینت نیست .ولی پیش فرض اش JQUERY و بوت استرپ استفاده می‌کنه. شما می‌تونید N تا فریم ورک و قالب متفاوت تعریف کنید , به طوری که برای مثال یه صفحه وبسایتتون با React و صفحه دیگه با انگولار باشه .

برای دیباگ تحت وب هم امکاناتی در اختیار برنامه نویس قرار می‌ده مثل ریموت دیباگ و یا دیباگ در ویژوال استدیو.این‌ها بخشی از امکانات پلتفرم خودکاره برای آشنایی بیشتر با امکانات پلتفرم از لینک‌های زیر استفاده کنید.

حدود 70 ساعت آموزش در آپارات  :


خودم تا حالا دو تا وبسایت باهاش نوشتم و البته چند تا دیگه هم در دست توسعه است. وبسایت های کارشناسان.نت با قالب بوت استرپ و jquery  نوشته شده و داستان موفقیت با قالب Material و jquer نوشته شده است.

دوستان عزیز قصد دارم ورژن core پلتفرم خودکار  رو بنویسم و یه سری امکانات جدید مثل کامپایلر و BuildSystem ری اکت رو اضافه کنم و هوش مصنوعی IDE رو هم افزایش بدم. اگر علاقه مند به همکاری هستید برام پیام بذارید.

با توجه به متن باز بودن پلتفرم , سود مالی وجود نداره ولی می‌تونه رزومه خوبی براتون بشه و از اون مهمتره می‌تونه پروژه فوق العاده ارزشمند و کاربردی باشه. 

پلتفرم خودکار یک پلتفرم رایگان و متن باز (Open Source) برای تولید نرم افزار تحت وب با تکنولوژی مایکروسافت